{"openapi":"3.1.0","servers":[{"url":"https://api.spice.ai"}],"info":{"title":"Spice Cloud Control-Plane API","version":"v1","description":"The Spice Cloud Platform API provides programmatic access to manage projects, deployments, secrets, API keys, and organization members on the Spice.ai cloud platform. Projects were previously called apps: every `/v1/projects` endpoint is also served at the legacy `/v1/apps` path.\n\n## Organization context\n\nA request acts on one organization. By default that is the organization the credential was minted against; send `X-Org-Name` with an organization handle to act on another. A user credential may name any organization its owner belongs to. A machine credential (OAuth client credentials) is issued to one organization and stays pinned to it: naming another is refused, never ignored. A route under `/v1/orgs/{orgId}` takes the org from that id. On those routes, `X-Org-Name` must name the same org.\n\nMembers, limits, projects, and clusters use that same path. For example, list projects with `GET /v1/orgs/{orgId}/projects`. The earlier paths still serve those operations. On an earlier path, the org comes from the credential or from `X-Org-Name`.\n\nRegions are the same for every org. Cloud Connect keeps `X-Org-Name` because the enrollment signature includes the org handle.\n\nA portal session token is bound to no organization. It must send `X-Org-Name` on every route except `GET /v1/orgs` and a route under `/v1/orgs/{orgId}`.\n\nRefusals carry a machine-readable `code` alongside `error`, because the remedies differ: `org_forbidden` means the caller is not a member of the named organization (ask an owner for an invitation), `forbidden` means they are a member but lack the role the action needs, `insufficient_scope` means the credential was not granted it, `org_assertion_mismatch` means a machine credential named an organization other than its own, `org_deleted` means the organization the credential itself is bound to has been deleted (no invitation or role reaches it again), `invalid_org_assertion` (400) means the header was not a valid handle, and `org_assertion_required` (400) means a portal session named no organization on a route that needs one. `invalid_org_id` (400) means the path `orgId` is not a positive integer. `org_path_mismatch` (400) means `X-Org-Name` does not name that org.\n\nA request made with a portal session token is answered with `503` when Supabase Auth or its signing keys do not answer within 5 seconds, since neither the signature nor the session can then be confirmed."},"paths":{"/v1/health":{"get":{"summary":"Health check","description":"Returns the health status of the API.","responses":{"200":{"description":"API is healthy","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"ok"},"timestamp":{"type":"string","format":"date-time"}}}}}}},"tags":["Health"]}},"/v1/cloud-connect/token":{"post":{"summary":"Issue a Cloud Connect enrollment token","description":"Issues a one-time standalone or cluster enrollment token. Requires a user-derived bearer, X-Org-Name, the kind-appropriate write scope, and org admin/owner role. Client-credential tokens are rejected because enrollment admission must retain user audit attribution.","security":[{"BearerAuth":[]}],"parameters":[{"in":"header","name":"X-Org-Name","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"type":"object","additionalProperties":false,"required":["kind"],"properties":{"kind":{"type":"string","enum":["standalone"]},"ttl_seconds":{"type":"integer","minimum":1,"maximum":86400,"default":3600}}},{"type":"object","additionalProperties":false,"required":["kind","name"],"properties":{"kind":{"type":"string","enum":["cluster"]},"name":{"type":"string"},"region":{"type":"string"},"cloud_account_id":{"type":"string"},"cloud_provider":{"type":"string","enum":["aws"]},"operations_mode":{"type":"string","enum":["managed","approval_required"]},"ttl_seconds":{"type":"integer","minimum":1,"maximum":86400,"default":3600}}},{"type":"object","additionalProperties":false,"required":["kind","connection_id"],"properties":{"kind":{"type":"string","enum":["cluster"]},"connection_id":{"type":"string"},"ttl_seconds":{"type":"integer","minimum":1,"maximum":86400,"default":3600}}}]}}}},"responses":{"201":{"description":"Enrollment token issued","content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["kind","token","expires_at","organization","target"],"properties":{"kind":{"type":"string","enum":["standalone","cluster"]},"token":{"type":"string","pattern":"^spice-enroll-[A-Za-z0-9_-]{32}$"},"expires_at":{"type":"string","format":"date-time"},"organization":{"type":"object","additionalProperties":false,"required":["id","name"],"properties":{"id":{"type":"integer"},"name":{"type":"string"}}},"target":{"oneOf":[{"type":"object","description":"The created cluster or rotated connection identifier."},{"type":"null"}]}}}}}},"400":{"description":"Invalid request or missing organization assertion"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope, role, or plan"},"409":{"description":"Cluster is decommissioned"},"413":{"description":"Request body too large"},"429":{"description":"Token issuance rate limit exceeded"},"500":{"description":"Internal token issuance failure"}},"tags":["Cloud Connect"]}},"/v1/cloud-connect/enroll":{"post":{"summary":"Enroll a Cloud Connect cluster or standalone runtime","description":"Token enrollment accepts a canonical kind-bound enrollment token. Standalone enrollment may instead use user Authorization plus X-Org-Name. Idempotency-Key is required for standalone enrollment.","security":[{},{"BearerAuth":[]}],"parameters":[{"in":"header","name":"Idempotency-Key","required":false,"schema":{"type":"string","format":"uuid"}},{"in":"header","name":"X-Org-Name","required":false,"description":"Required with Bearer authorization for authenticated standalone enrollment; omitted for token enrollment.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"type":"object","additionalProperties":false,"required":["kind","token","csr_pem","enc_pubkey_pem","instance"],"properties":{"kind":{"type":"string","enum":["standalone"]},"token":{"type":"string","pattern":"^spice-enroll-[A-Za-z0-9_-]{32}$"},"expected_org":{"type":"string"},"csr_pem":{"type":"string"},"enc_pubkey_pem":{"type":"string"},"instance":{"type":"object"},"region":{"type":"string"}}},{"type":"object","additionalProperties":false,"required":["kind","token","csr_pem","enc_pubkey_pem"],"properties":{"kind":{"type":"string","enum":["cluster"]},"token":{"type":"string","pattern":"^spice-enroll-[A-Za-z0-9_-]{32}$"},"expected_org":{"type":"string"},"csr_pem":{"type":"string"},"enc_pubkey_pem":{"type":"string"}}},{"type":"object","additionalProperties":false,"required":["kind","csr_pem","enc_pubkey_pem","instance"],"properties":{"kind":{"type":"string","enum":["standalone"]},"csr_pem":{"type":"string"},"enc_pubkey_pem":{"type":"string"},"instance":{"type":"object"},"region":{"type":"string"}}}]}}}},"responses":{"200":{"description":"Enrollment completed or exactly replayed","content":{"application/json":{"schema":{"oneOf":[{"type":"object","additionalProperties":false,"required":["kind","instance_id","identity_cert_pem","ca_bundle_pem","gateway_addr","not_after","organization","region","portal","attachment","recovered"],"properties":{"kind":{"type":"string","enum":["standalone"]},"instance_id":{"type":"string"},"identity_cert_pem":{"type":"string"},"ca_bundle_pem":{"type":"string"},"gateway_addr":{"type":"string"},"not_after":{"type":"string","format":"date-time"},"organization":{"type":"object","required":["id","name"],"properties":{"id":{"type":"integer"},"name":{"type":"string"}}},"region":{"type":"string"},"portal":{"type":"object","required":["new_project_url"],"properties":{"new_project_url":{"type":"string","format":"uri"}}},"attachment":{"type":"null"},"recovered":{"type":"boolean"}}},{"type":"object","additionalProperties":false,"required":["kind","connection_id","identity_cert_pem","ca_bundle_pem","gateway_addr","not_after","organization","region","attachment"],"properties":{"kind":{"type":"string","enum":["cluster"]},"connection_id":{"type":"string"},"identity_cert_pem":{"type":"string"},"ca_bundle_pem":{"type":"string"},"gateway_addr":{"type":"string"},"not_after":{"type":"string","format":"date-time"},"organization":{"type":"object","required":["id","name"],"properties":{"id":{"type":"integer"},"name":{"type":"string"}}},"region":{"type":"string"},"attachment":{"type":"null"}}}]}}}},"400":{"description":"Invalid request, kind mismatch, or unsupported project field"},"401":{"description":"Invalid credential or authorization"},"403":{"description":"Authenticated caller lacks the required scope or organization role"},"409":{"description":"Consumed token, connection already enrolled, unavailable connection, organization mismatch, or idempotency mismatch"},"410":{"description":"Enrollment token or authenticated recovery window expired"},"413":{"description":"Request body too large"},"500":{"description":"Internal or identity-provider failure; use the retryable response field"}},"tags":["Cloud Connect"]}},"/v1/cloud-connect/attach":{"post":{"summary":"Attach an existing project to an enrolled instance","description":"Attaches an existing standalone project to an enrolled standalone instance and assigns the project's region and cname from the stamp whose gateway terminates that instance's control stream. The optional location field is reserved for multi-region use and is ignored today.\nThis operation is dual-authorized. A user-derived bearer plus the X-Org-Name assertion authorizes the project mutation; OAuth client credentials are refused. The caller must have the instances:write and apps:write scopes and the organization admin or owner role. The cert_pem and pop_sig body fields prove only that the request concerns the named instance. Instance proof by itself never authorizes project attachment.\npop_sig is a base64 DER ECDSA (SHA-256) signature over the UTF-8 bytes `spice-cloud-connect/attach/v1\\n{X-Org-Name}\\n{instance_id}\\n{project_id}`. The project must already have kind standalone and no cluster target. The database is the authority for attachment conflicts; clients must not pre-check or infer an attachment limit from this contract.\nErrors use `{code, error, retryable}`. A 409 always has `retryable: false`; a 5xx has `retryable: true` so the client exact replays any request whose attachment may already have committed.\n","security":[{"BearerAuth":[]}],"parameters":[{"in":"header","name":"X-Org-Name","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"},"description":"Organization that owns both the project and instance; also bound into pop_sig"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["instance_id","project_id","cert_pem","pop_sig"],"properties":{"instance_id":{"type":"string","minLength":1,"maxLength":200,"description":"Enrolled instance caller-facing id (`inst_…`)"},"project_id":{"type":"integer","minimum":1,"maximum":9007199254740991,"description":"Existing standalone project id"},"location":{"type":"string","minLength":1,"maxLength":200,"description":"Reserved for multi-region use; accepted and ignored today"},"cert_pem":{"type":"string","minLength":1,"maxLength":8192,"description":"Cloud Connect identity certificate for instance_id"},"pop_sig":{"type":"string","minLength":1,"maxLength":512,"description":"Base64 DER ECDSA signature over the documented attach challenge"}}}}}},"responses":{"200":{"description":"Project attached, or exact same instance/project replay repaired","content":{"application/json":{"schema":{"type":"object","required":["instance_id","organization","project","monitor_url"],"properties":{"instance_id":{"type":"string"},"organization":{"type":"object","required":["id","name"],"properties":{"id":{"type":"integer"},"name":{"type":"string","description":"Canonical organization name"}}},"project":{"type":"object","required":["id","name"],"properties":{"id":{"type":"integer"},"name":{"type":"string","description":"Canonical project name"}}},"monitor_url":{"type":"string","format":"uri","pattern":"^https://","description":"Credential-free portal monitoring link for the project"}}}}}},"400":{"description":"Invalid body or X-Org-Name (`invalid_request` or `invalid_org_assertion`)","content":{"application/json":{"schema":{"type":"object","required":["code","error","retryable"],"properties":{"code":{"type":"string","enum":["invalid_request","invalid_org_assertion","unauthenticated","forbidden","org_forbidden","org_assertion_mismatch","org_deleted","invalid_instance_proof","instance_not_found","instance_not_enrolled","instance_already_attached","project_not_found","project_not_standalone","project_already_attached","project_left_org","internal"]},"error":{"type":"string"},"retryable":{"type":"boolean"}}}}}},"401":{"description":"Missing/invalid user bearer or invalid instance proof (`unauthenticated` or `invalid_instance_proof`)","content":{"application/json":{"schema":{"type":"object","required":["code","error","retryable"],"properties":{"code":{"type":"string","enum":["invalid_request","invalid_org_assertion","unauthenticated","forbidden","org_forbidden","org_assertion_mismatch","org_deleted","invalid_instance_proof","instance_not_found","instance_not_enrolled","instance_already_attached","project_not_found","project_not_standalone","project_already_attached","project_left_org","internal"]},"error":{"type":"string"},"retryable":{"type":"boolean"}}}}}},"403":{"description":"Non-user bearer, insufficient scope, org access, or role refusal (`forbidden`, `org_forbidden`, `org_assertion_mismatch`, or `org_deleted`)","content":{"application/json":{"schema":{"type":"object","required":["code","error","retryable"],"properties":{"code":{"type":"string","enum":["invalid_request","invalid_org_assertion","unauthenticated","forbidden","org_forbidden","org_assertion_mismatch","org_deleted","invalid_instance_proof","instance_not_found","instance_not_enrolled","instance_already_attached","project_not_found","project_not_standalone","project_already_attached","project_left_org","internal"]},"error":{"type":"string"},"retryable":{"type":"boolean"}}}}}},"404":{"description":"Instance or project not found in the authorized organization (`instance_not_found` or `project_not_found`)","content":{"application/json":{"schema":{"type":"object","required":["code","error","retryable"],"properties":{"code":{"type":"string","enum":["invalid_request","invalid_org_assertion","unauthenticated","forbidden","org_forbidden","org_assertion_mismatch","org_deleted","invalid_instance_proof","instance_not_found","instance_not_enrolled","instance_already_attached","project_not_found","project_not_standalone","project_already_attached","project_left_org","internal"]},"error":{"type":"string"},"retryable":{"type":"boolean"}}}}}},"409":{"description":"Instance not enrolled or already attached, project not standalone, the database reports the project is already attached, or the project moved to another org during the attach (`instance_not_enrolled`, `instance_already_attached`, `project_not_standalone`, `project_already_attached`, or `project_left_org`). These refusals carry `retryable: false` and prove this request did not commit an attachment.\n","content":{"application/json":{"schema":{"type":"object","required":["code","error","retryable"],"properties":{"code":{"type":"string","enum":["invalid_request","invalid_org_assertion","unauthenticated","forbidden","org_forbidden","org_assertion_mismatch","org_deleted","invalid_instance_proof","instance_not_found","instance_not_enrolled","instance_already_attached","project_not_found","project_not_standalone","project_already_attached","project_left_org","internal"]},"error":{"type":"string"},"retryable":{"type":"boolean"}}}}}},"413":{"description":"Request body exceeds 1 MiB (`invalid_request`)","content":{"application/json":{"schema":{"type":"object","required":["code","error","retryable"],"properties":{"code":{"type":"string","enum":["invalid_request","invalid_org_assertion","unauthenticated","forbidden","org_forbidden","org_assertion_mismatch","org_deleted","invalid_instance_proof","instance_not_found","instance_not_enrolled","instance_already_attached","project_not_found","project_not_standalone","project_already_attached","project_left_org","internal"]},"error":{"type":"string"},"retryable":{"type":"boolean"}}}}}},"500":{"description":"Internal failure, including post-commit routing repair (`internal`, retryable)","content":{"application/json":{"schema":{"type":"object","required":["code","error","retryable"],"properties":{"code":{"type":"string","enum":["invalid_request","invalid_org_assertion","unauthenticated","forbidden","org_forbidden","org_assertion_mismatch","org_deleted","invalid_instance_proof","instance_not_found","instance_not_enrolled","instance_already_attached","project_not_found","project_not_standalone","project_already_attached","project_left_org","internal"]},"error":{"type":"string"},"retryable":{"type":"boolean"}}}}}}},"tags":["Cloud Connect"]}},"/v1/cloud-connect/attachable-projects":{"get":{"summary":"List projects available for Cloud Connect attachment","description":"Returns every project the authenticated caller may attach an enrolled Cloud Connect instance to. Policy is evaluated entirely by the server: today, results include only live, unattached standalone projects in the resolved organization, and user credentials require that organization's owner or admin role. Exactly one organization is searched per request and the response names it in `org`, so an empty `projects` states which organization has no eligible project rather than leaving a client to wonder whether the one it meant was searched at all; name another with `X-Org-Name`. Clients must render every returned project and instance without applying their own eligibility filters. The `instances` array is part of the stable contract and may be populated when server policy permits projects with existing attachments.\n","security":[{"BearerAuth":[]}],"parameters":[{"in":"header","name":"X-Org-Name","required":false,"schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"},"description":"Organization to list. A user token may name any organization its owner belongs to; naming one they do not is `org_forbidden`. Never ignored: a token bound to one organization that names another is `org_assertion_mismatch`. A portal session token is bound to none and must send it (`org_assertion_required`).\n"},{"in":"query","name":"limit","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"Maximum number of projects to return."},{"in":"query","name":"cursor","required":false,"schema":{"type":"string"},"description":"Opaque cursor returned as `next_cursor` by the previous page."}],"responses":{"200":{"description":"A page of projects currently eligible for attachment.","content":{"application/json":{"schema":{"type":"object","required":["org","projects","next_cursor"],"properties":{"org":{"type":"string","description":"Handle of the organization the page was scoped to, being the one `X-Org-Name` named or the credential's own when it named none. Always present, so an empty `projects` says which organization has no eligible project rather than leaving a client to guess whether its active organization was the one searched.\n"},"projects":{"type":"array","items":{"type":"object","required":["id","name","org","region","instances"],"properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"org":{"type":"string","description":"Organization handle."},"region":{"type":["string","null"],"description":"Project control-plane region, if assigned."},"instances":{"type":"array","items":{"type":"object","required":["id","location","enrolled_at"],"properties":{"id":{"type":"string","description":"Instance external identifier."},"location":{"type":["string","null"],"description":"Customer-declared instance location."},"enrolled_at":{"type":["string","null"],"format":"date-time"}}}}}}},"next_cursor":{"type":["string","null"],"description":"Opaque cursor for the next page, or null at the end."}}}}}},"400":{"description":"Invalid organization assertion (`invalid_org_assertion`), none from a portal session (`org_assertion_required`), or invalid limit/cursor (`invalid_pagination`).\n"},"401":{"description":"Missing or invalid bearer token."},"403":{"description":"Organization access (`org_forbidden`, `org_assertion_mismatch`, or `org_deleted`), apps:read scope (`insufficient_scope`), or owner/admin role (`forbidden`) was refused.\n"},"500":{"description":"Organization role, project, or organization-handle lookup failed.\n"}},"tags":["Cloud Connect"]}},"/v1/projects":{"get":{"summary":"List projects","description":"Returns projects belonging to the authenticated organization. Page with `limit` (default 1000, max 1000). Prefer `after_id` for a stable keyset (descending `id`). `offset` is accepted for compatibility. PostgREST also caps a page at 1000 rows.\n","security":[{"BearerAuth":[]}],"parameters":[{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":1000,"default":1000},"description":"Maximum number of projects to return"},{"in":"query","name":"offset","schema":{"type":"integer","minimum":0,"default":0},"description":"Number of projects to skip when a cursor is not supplied"},{"in":"query","name":"after_id","schema":{"type":"integer","minimum":1},"description":"Return projects with a strictly smaller id than this value."},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"List of projects","content":{"application/json":{"schema":{"type":"object","properties":{"projects":{"type":"array","items":{"$ref":"#/components/schemas/Project"}}}}}}},"400":{"description":"Invalid limit, offset, or after_id"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"}},"tags":["Projects"]},"post":{"summary":"Create a project","description":"Creates a new project in the authenticated organization.","security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":4,"pattern":"^[a-zA-Z0-9-]+$","description":"Project name (letters, numbers, hyphens only)"},"region":{"type":"string","enum":["us-east-1","us-west-2"],"description":"Cloud region where a managed project will be deployed (e.g. `us-east-1`). Omit this, `cname`, and `cluster_name` to create an unattached standalone project whose placement is resolved when linked.\n"},"cname":{"type":"string","description":"**Deprecated.** Internal region CNAME (e.g. `us-east-1-prod-aws-data`). Use `region` instead. Accepted for backwards compatibility when `region` is omitted. Omit all region sources to create an unattached standalone project.\n"},"cluster_name":{"type":"string","description":"Name of the dedicated cluster to deploy to, as listed by `GET /v1/clusters`. Provide exactly one of `region` or `cluster_name`. When set, the region/routing are derived from the cluster; if `region` is also provided it must match the cluster's region. Omit all region sources to create an unattached standalone project.\n"},"description":{"type":"string","description":"Project description"},"visibility":{"type":"string","enum":["public","private"],"default":"private"},"default_alerts":{"type":"boolean","default":true,"description":"Create default monitoring alerts for the new managed project. False skips initial setup. Manual alert creation remains available."},"tags":{"type":"object","additionalProperties":{"type":"string"},"description":"Key-value tags for the project"},"update_channel":{"type":"string","enum":["stable","preview","nightly"],"description":"Update channel for a managed project deployment. A standalone project has no spicepod until linking, so configure its channel after linking."},"replicas":{"type":"integer","minimum":0,"description":"Number of replicas for a managed project. Rejected for standalone projects."},"resources":{"type":"object","description":"Resource requests and limits for a managed project container. Rejected for standalone projects.","properties":{"limits":{"type":"object","properties":{"cpu":{"type":"string","description":"Whole-number vCPU limit, or '-' for no CPU limit"},"memory":{"type":"string","description":"Memory limit in Gi (for example, 16Gi)"},"ephemeral-storage":{"type":"string","description":"Ephemeral storage limit in Gi (for example, 8Gi)"}}},"requests":{"type":"object","properties":{"cpu":{"type":"string","description":"CPU request. Minimum 100 millicores (for example, 100m or 0.1)."}}}}},"executor":{"type":"object","description":"Executor container configuration for a managed project. Rejected for standalone projects.","properties":{"replicas":{"type":"integer","minimum":0,"description":"Number of executor replicas"},"resources":{"type":"object","description":"Resource requests and limits for the executor container","properties":{"limits":{"type":"object","properties":{"cpu":{"type":"string","description":"Whole-number vCPU limit, or '-' for no CPU limit"},"memory":{"type":"string","description":"Memory limit in Gi (for example, 16Gi)"},"ephemeral-storage":{"type":"string","description":"Ephemeral storage limit in Gi (for example, 8Gi)"}}},"requests":{"type":"object","properties":{"cpu":{"type":"string","description":"CPU request. Minimum 100 millicores (for example, 100m or 0.1)."}}}}}}}}}}}},"responses":{"201":{"description":"Project created successfully. The response may include a `warning` field if initial configuration could not be applied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectWithConfig"}}}},"400":{"description":"Invalid request body or invalid region"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope, dedicated-cluster assignment or special-org private-compute ceilings required for resource limits, or requested resources exceed dedicated-cluster capacity"},"404":{"description":"Organization not found, including an organization deleted while the project was being created"},"409":{"description":"A project with this name already exists, or the project was deleted while its initial configuration was being written (`code` is `project_deleted`)"},"429":{"description":"Too many project creation requests (rate limited)"},"500":{"description":"Project creation failed, including when the name-collision check could not be read","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"Generic failure reason; the underlying cause stays in the server logs","example":"Failed to create project"}}}}}}},"tags":["Projects"],"parameters":[{"$ref":"#/components/parameters/OrgName"}]}},"/v1/projects/{projectId}":{"get":{"summary":"Get a project","description":"Returns details for a specific project, including its configuration.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"Project details with configuration","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"},"kind":{"type":"string","enum":["managed","standalone","byoc"],"description":"Resolved project hosting model"},"description":{"type":"string"},"visibility":{"type":"string","enum":["public","private"]},"created_at":{"type":"string","format":"date-time"},"cluster_name":{"type":["string","null"],"description":"Resolved dedicated cluster name; null for projects not assigned to a dedicated cluster."},"endpoint":{"type":["string","null"],"description":"The data-plane endpoint the project connects to, or null before a standalone instance is attached."},"production_branch":{"type":"string"},"api_key":{"type":"string","deprecated":true,"description":"Deprecated. Use the API Keys endpoints instead."},"tags":{"type":"object","additionalProperties":{"type":"string"}},"forked_from":{"type":["object","null"],"description":"The project this project was forked from, while that project is live in the same organization. Null for a project that was not created as a fork.\n","required":["id","name"],"properties":{"id":{"type":"integer","description":"ID of the source project"},"name":{"type":"string","description":"Name of the source project"}}},"paused_at":{"type":["string","null"],"format":"date-time","description":"Timestamp when the project was paused; null while running"},"config":{"type":"object","properties":{"spicepod":{"type":"object"},"image_tag":{"type":"string","description":"Runtime image tag"},"update_channel":{"type":["string","null"],"enum":["stable","preview","nightly",null],"description":"Update channel for the runtime, or null when Spice operates this project's runtime updates."},"version":{"type":"string","description":"Semver range for the runtime version (e.g. 1.x, 2.x)"},"replicas":{"type":"integer"},"resources":{"type":["object","null"],"description":"Resource requests and limits for the project container","properties":{"limits":{"type":"object","properties":{"cpu":{"type":"string","description":"Whole-number vCPU limit, or '-' for no CPU limit"},"memory":{"type":"string","description":"Memory limit in Gi (for example, 16Gi)"},"ephemeral-storage":{"type":"string"}}},"requests":{"type":"object","properties":{"cpu":{"type":"string","description":"CPU request. Minimum 100 millicores (for example, 100m or 0.1)."}}}}},"executor":{"type":["object","null"],"description":"Executor container configuration","properties":{"replicas":{"type":"integer","description":"Number of executor replicas"},"resources":{"type":"object","description":"Resource requests and limits for the executor container","properties":{"limits":{"type":"object","properties":{"cpu":{"type":"string","description":"Whole-number vCPU limit, or '-' for no CPU limit"},"memory":{"type":"string","description":"Memory limit in Gi (for example, 16Gi)"},"ephemeral-storage":{"type":"string"}}},"requests":{"type":"object","properties":{"cpu":{"type":"string","description":"CPU request. Minimum 100 millicores (for example, 100m or 0.1)."}}}}},"storage_size_gb":{"type":["number","null"]}}},"region":{"type":["string","null"],"description":"Server-resolved region; null before a standalone instance is attached."},"storage_size_gb":{"type":["number","null"]},"storage_claim_size_gb":{"type":"number","deprecated":true,"description":"Deprecated. Use storage_size_gb instead."}}}}}}}},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"},"404":{"description":"Project not found"},"500":{"description":"Internal server error"}},"tags":["Projects"]},"put":{"summary":"Update a project","description":"Updates a project's metadata and configuration, including description, visibility, spicepod configuration, replicas, and resource limits.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"$ref":"#/components/parameters/OrgName"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"description":{"type":"string"},"visibility":{"type":"string","enum":["public","private"]},"production_branch":{"type":"string"},"tags":{"type":"object","additionalProperties":{"type":"string"},"description":"Key-value tags for the project. The map replaces the stored tags. System tags the project already has stay on the project when the map omits them.\n"},"spicepod":{"oneOf":[{"type":"string","description":"YAML string of the spicepod configuration"},{"type":"object","description":"JSON object of the spicepod configuration"}],"description":"The project's Spicepod. A project connected to a GitHub repository takes its Spicepod from the repository's spicepod.yaml, so for such a project only the stored Spicepod, sent back unchanged, is accepted."},"image_tag":{"type":["string","null"],"description":"Runtime image tag for a managed project; rejected for standalone projects. A non-empty value pins the project. On stable, the pin must be a published stable runtime version. On preview, it must be a published preview or stable runtime version. On stable or preview, send null to clear a pin so deploys resolve from the channel catalog (stable - latest GA for the configured version range; preview - latest RC). null is rejected on nightly and internal channels."},"update_channel":{"type":"string","enum":["stable","preview","nightly"],"description":"Update channel for a managed runtime; rejected for standalone projects."},"version":{"type":"string","description":"Semver range for a managed runtime version (e.g. 1.x, 2.x); rejected for standalone projects."},"replicas":{"type":"integer","minimum":0,"description":"Managed runtime replica count; rejected for standalone projects."},"resources":{"type":"object","description":"Resource requests and limits for a managed project container; rejected for standalone projects.","properties":{"limits":{"type":"object","properties":{"cpu":{"type":"string","description":"Whole-number vCPU limit, or '-' for no CPU limit"},"memory":{"type":"string","description":"Memory limit in Gi (for example, 16Gi)"},"ephemeral-storage":{"type":"string","description":"Ephemeral storage limit in Gi (for example, 8Gi)"}}},"requests":{"type":"object","properties":{"cpu":{"type":"string","description":"CPU request. Minimum 100 millicores (for example, 100m or 0.1)."}}}}},"executor":{"type":"object","description":"Executor container configuration for a managed project; rejected for standalone projects.","properties":{"replicas":{"type":"integer","minimum":0,"description":"Number of executor replicas"},"resources":{"type":"object","description":"Resource requests and limits for the executor container","properties":{"limits":{"type":"object","properties":{"cpu":{"type":"string","description":"Whole-number vCPU limit, or '-' for no CPU limit"},"memory":{"type":"string","description":"Memory limit in Gi (for example, 16Gi)"},"ephemeral-storage":{"type":"string","description":"Ephemeral storage limit in Gi (for example, 8Gi)"}}},"requests":{"type":"object","properties":{"cpu":{"type":"string","description":"CPU request. Minimum 100 millicores (for example, 100m or 0.1)."}}}}},"storage_size_gb":{"type":"number"}}},"region":{"type":"string","description":"Managed project region; standalone placement is resolved during attachment."},"cluster_name":{"type":"string","description":"Reassign the project to a dedicated cluster, as listed by `GET /v1/clusters`. Provide at most one of `region` or `cluster_name`. When set, region/routing are derived from the cluster; if `region` is also provided it must match the cluster's region. Rejected for standalone projects, whose placement is resolved during attachment.\n"},"storage_size_gb":{"type":"number","description":"Managed runtime storage size; rejected for standalone projects."},"storage_claim_size_gb":{"type":"number","deprecated":true,"description":"Deprecated. Use storage_size_gb instead. Rejected for standalone projects."}}}}}},"responses":{"200":{"description":"Project updated successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectWithConfig"}}}},"400":{"description":"Invalid request body or project ID, including placement or hosted runtime fields that do not apply to standalone projects. When `image_tag` is supplied on stable or preview, a tag that is not a published version for that channel returns `{ error: 'Invalid image_tag for stable channel', code: 'invalid_stable_image_tag' }` (or the preview analog). A `spicepod` that adds a component on a source the Spice runtime no longer loads returns code `retired_component_source`; components the project already stores on such a source may be kept, edited and renamed, and a project backed by a GitHub repository is not checked. A `spicepod` that adds a plain-text secret (a password, token, API key, private key, or URL password in a component param) returns code `plaintext_secrets` with `plaintext_secrets` listing each path. Store the value as a project or org secret and use `${secrets:NAME}` in its place. Secrets the stored spicepod already holds do not block the update."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope, viewer role cannot mutate projects, requested replicas or resources exceed plan limits, dedicated-cluster assignment or special-org private-compute ceilings required, requested resources exceed dedicated-cluster capacity, `image_tag` was supplied outside the Enterprise plan (code: `image_tag_requires_enterprise`), or the requested runtime image is not available for the organization plan (code: `image_tag_not_allowed_for_plan`); omitting `image_tag` leaves the configured image unchanged"},"404":{"description":"Project not found"},"409":{"description":"The project's lifecycle state changed while the update was in flight (a pause, resume, or restore landed at the same time), so the configuration was not saved (code: `project_state_changed`). Re-read the project before retrying: a configuration update clears `paused_at`, so repeating this request against a project that has since been paused also resumes it. Or a `spicepod` that differs from the stored one was sent for a project connected to a GitHub repository (code: `github_connected`); change spicepod.yaml in the repository instead, since nothing in that request is applied."},"500":{"description":"Internal server error"},"502":{"description":"The configuration was stored but could not be published to the spicepod registry (code: `registry_publish_failed`), so the project still deploys its previously published configuration. Retry the request."},"503":{"description":"Runtime version catalog is unavailable"}},"tags":["Projects"]},"delete":{"summary":"Delete a project","description":"Deletes a project and tears down its runtime resources.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project to delete"},{"$ref":"#/components/parameters/OrgName"}],"responses":{"204":{"description":"Project deleted successfully"},"400":{"description":"Invalid project ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope or viewer role cannot mutate projects"},"404":{"description":"Project not found"},"500":{"description":"Internal server error"}},"tags":["Projects"]}},"/v1/projects/{projectId}/forks":{"get":{"summary":"List forks of a project","description":"Returns the live projects in the organization that were forked from this project, ordered by name. Requires `apps:read`.\n","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the source project"},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"Forks of the project","content":{"application/json":{"schema":{"type":"object","required":["forks"],"properties":{"forks":{"type":"array","items":{"$ref":"#/components/schemas/Project"}}}}}}},"400":{"description":"Invalid project ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"},"404":{"description":"Project not found"},"500":{"description":"Internal server error"}},"tags":["Projects"]},"post":{"summary":"Fork a project","description":"Creates a new project from this project's configuration and records this project as its source. The fork copies the spicepod the project deploys, its linked connections, project secrets, linked organization secrets, update channel, version range, replica and executor counts, storage size, description, and tags. It does not copy API keys (new ones are generated), deployments, the GitHub repository link, the instance size, or visibility (forks are private). The fork is not deployed: deploy it with POST /v1/projects/{projectId}/deployments. Requires `apps:write`. The server copies secret values without returning them, so no secrets scope is needed. A source connected to a GitHub repository is read from that repository, and its spicepod is copied only when it loads no other file from the repository; to deploy a fork from a fork of the repository, fork the project in the Spice portal.\n","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project to fork"},{"$ref":"#/components/parameters/OrgName"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"name":{"type":"string","minLength":4,"maxLength":38,"pattern":"^[a-zA-Z0-9-]+$","description":"Name of the fork. Defaults to the source name with `-fork`, then `-fork-2` and so on, whichever is free.\n"},"region":{"type":"string","enum":["us-east-1","us-west-2"],"description":"Region to place the fork in. Omit it and `cluster_name` to place the fork where the source runs.\n"},"cluster_name":{"type":"string","description":"Dedicated cluster to place the fork on, as listed by `GET /v1/clusters`. If `region` is also set, it must match the cluster's region.\n"},"scheduler_state_location":{"type":"string","maxLength":2048,"description":"s3:// URI for the fork's scheduler state. Required when the source spicepod sets `runtime.scheduler.state_location`, and must not overlap it: two schedulers cannot share a location.\n"},"default_alerts":{"type":"boolean","default":true,"description":"Create default monitoring alerts for the new managed fork. False skips initial setup. Manual alert creation remains available."}}}}}},"responses":{"201":{"description":"Project forked. It has not been deployed yet.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ProjectWithConfig"},{"type":"object","required":["shared_state"],"properties":{"shared_state":{"type":"array","description":"Settings copied as they are that name external state the source also uses: a replication slot or Kafka consumer group the spicepod names, or a snapshot location. Change them in the fork's spicepod before deploying it if the two projects should not share that state.\n","items":{"type":"object","required":["component","setting","value"],"properties":{"component":{"type":"string","description":"The dataset, or `snapshots`"},"setting":{"type":"string","example":"pg_replication_slot"},"value":{"type":"string"}}}}}}]}}}},"400":{"description":"Invalid project ID or request body, an unknown region or cluster, no `region` or `cluster_name` set for a source whose placement a fork cannot reuse (a BYOC cluster, a self-hosted runtime, or a region or cluster that is no longer available; `code: fork_placement_required`), or a missing or unusable `scheduler_state_location` (`code: scheduler_state_location_required`, `scheduler_state_location_unsupported`, `scheduler_state_location_overlaps_source`, or `scheduler_state_location_not_applicable`).\n"},"401":{"description":"Unauthorized"},"402":{"description":"The source runs models on hosted AI credits, which the organization has used up (`code: ai_credits_exhausted`).\n"},"403":{"description":"Insufficient scope, a viewer role, or the source runs more replicas or executors than the plan or the dedicated cluster allows\n"},"404":{"description":"Project or organization not found"},"409":{"description":"A project with the requested name already exists, another project creation is in progress, or every default name is taken (`code: fork_name_unavailable`)\n"},"413":{"description":"Request body too large"},"422":{"description":"The source has no spicepod (`code: fork_source_has_no_spicepod`), its spicepod is not valid (`code: fork_source_spicepod_invalid`), or its spicepod loads files from its GitHub repository besides spicepod.yaml (`code: fork_source_loads_repository_files`)\n"},"429":{"description":"Too many project creation requests (rate limited)"},"500":{"description":"The fork could not be created. When the failure comes after the project row is created, the fork is deleted again with its spicepod, secrets, and organization secret links. If the project itself cannot be deleted, the response has `code: fork_rollback_failed` and names the partially created project in `project` (`id` and `name`). Delete that project, by `project.id`, before reusing its name.\n"},"502":{"description":"The source is connected to a GitHub repository, and its spicepod could not be read from it (`code: fork_source_repository_unreadable`)\n"}},"tags":["Projects"]}},"/v1/projects/{projectId}/pause":{"post":{"summary":"Pause a project","description":"Pauses the project's running spicepod. The runtime is torn down and the\nspicepod record is marked as paused. Settings and configuration are\npreserved. Resume with POST /v1/projects/{projectId}/resume. Requires\n`apps:write`.\n","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project to pause"},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"Project paused","content":{"application/json":{"schema":{"type":"object","required":["id","name","paused_at"],"properties":{"id":{"type":"integer","description":"The project ID"},"name":{"type":"string","description":"The project name"},"paused_at":{"type":"string","format":"date-time","description":"Timestamp when the project was paused"}}}}}},"400":{"description":"Invalid project ID, missing API key, no spicepod configured, or spicepod has been deleted"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"},"404":{"description":"Project not found"},"409":{"description":"Project is already paused","content":{"application/json":{"schema":{"type":"object","required":["error","paused_at"],"properties":{"error":{"type":"string"},"paused_at":{"type":"string","format":"date-time","description":"Timestamp of the existing pause"}}}}}},"500":{"description":"Internal server error"}},"tags":["Projects"]}},"/v1/projects/{projectId}/resume":{"post":{"summary":"Resume a paused project","description":"Clears the paused state on the project's spicepod and creates a new\ndeployment to bring the runtime back up. A project connected to a\nGitHub repository deploys spicepod.yaml from its root directory on its\nproduction branch, read when the project resumes. Requires\n`apps:write`.\n","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project to resume"},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"Project resumed, deployment created","content":{"application/json":{"schema":{"type":"object","required":["id","name","paused_at","deployment_id"],"properties":{"id":{"type":"integer","description":"The project ID"},"name":{"type":"string","description":"The project name"},"paused_at":{"type":["string","null"],"format":"date-time","description":"Always null after a successful resume"},"deployment_id":{"type":"integer","description":"ID of the deployment created to bring the runtime back up"}}}}}},"400":{"description":"Invalid project ID, missing API key, missing project organization, no spicepod configured, spicepod has been deleted, or the spicepod `name`, `kind` or `version` is missing or not valid (the body carries an `issues` array of `{path, message}`). For a project connected to a GitHub repository, also returned when the root directory has no spicepod.yaml or it is invalid or too large (`code: spicepod_not_found`, `spicepod_invalid` or `spicepod_too_large`). The project stays paused.\n"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope, or the org's GitHub App installation cannot read the connected repository (`code: github_unauthorized` or `github_forbidden`). Also returned when the installation was removed (`code: github_installation_removed`) or suspended (`code: github_installation_suspended`), or when it cannot read the connected repository because the repository is outside its repository access (`code: github_repository_unreachable`). The project stays paused.\n"},"404":{"description":"Project not found, or the connected GitHub repository or its production branch was not found (`code: github_not_found`). The project stays paused.\n"},"409":{"description":"Project is not paused, or its spicepod was deleted after the request read it. Also returned when another deployment of the project is queued (`code: deployment_in_progress`); the project then stays paused. Also returned when the project's repository, root directory, or production branch changes while the spicepod is read from the repository; the project then stays paused. Resume again.\n"},"422":{"description":"No runtime version in the catalog matches this project’s version range (`code: version_no_match`), or the spicepod uses version v1beta1, which the Spice 2 runtime cannot load (`code: spicepod_version_unsupported`). Also returned when the spicepod uses a connection credential that holds a secret reference deploy cannot resolve (`code: connection_credentials_unresolved`, with an `unresolved_credentials` array of `{connection, field}`). The project stays paused.\n"},"429":{"description":"GitHub rate-limited the read of the connected repository (`code: github_rate_limited`). The project stays paused.\n"},"500":{"description":"Internal server error"},"502":{"description":"GitHub did not answer while the connected repository was read (`code: github_unavailable`). The project stays paused.\n"},"503":{"description":"The runtime version catalog is unavailable or empty (`code: version_source_unavailable`), so an unpinned stable or preview resume cannot resolve an image tag.\n"}},"tags":["Projects"]}},"/v1/projects/{projectId}/repository":{"put":{"summary":"Connect a project to a GitHub repository","description":"Connects the project to a repository in the organization's GitHub\naccount, through the organization's Spice Cloud GitHub App\ninstallation. The next deployment reads spicepod.yaml (or\nspicepod.yml) from `root_directory` on `production_branch`.\nConnecting sets the project description to the repository\ndescription. Connecting again replaces the repository, root directory\nand production branch.\nRequires `apps:write`.\n","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project to connect"},{"$ref":"#/components/parameters/OrgName"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["repository"],"properties":{"repository":{"type":"string","description":"Repository in the organization's GitHub account, as `name` or `owner/name`. The owner must be the organization's GitHub account.","example":"cookbook"},"root_directory":{"type":["string","null"],"maxLength":512,"description":"Directory inside the repository that contains spicepod.yaml. Omit it, or send null or an empty string, for the repository root.","example":"cayenne"},"production_branch":{"type":"string","maxLength":255,"description":"Branch that deployments read. Defaults to the repository's default branch. A branch that GitHub renamed is stored under its new name.","example":"trunk"}}}}}},"responses":{"200":{"description":"Project connected","content":{"application/json":{"schema":{"type":"object","required":["project_id","repository","repository_id","root_directory","production_branch","connected_at","has_spicepod"],"properties":{"project_id":{"type":"integer","description":"The project ID"},"repository":{"type":"string","description":"The repository as `owner/name`"},"repository_id":{"type":"integer","description":"The GitHub repository ID"},"root_directory":{"type":["string","null"],"description":"The stored root directory. Null is the repository root."},"production_branch":{"type":"string","description":"The branch that deployments read"},"connected_at":{"type":"string","format":"date-time","description":"When the project was connected"},"has_spicepod":{"type":"boolean","description":"True when spicepod.yaml or spicepod.yml exists in the root directory on the production branch. A deployment fails until it does."}}}}}},"400":{"description":"Invalid project ID or request body, a repository outside the organization's GitHub account (code: `invalid_repository`), or a root directory outside the repository (code: `invalid_root_directory`)"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope, or viewer role cannot mutate projects"},"404":{"description":"Project not found, or the organization's GitHub App installation cannot read the repository (code: `repository_not_found`)"},"409":{"description":"The organization has no usable GitHub App installation (code: `github_not_connected`), or another project already deploys this repository and root directory (code: `root_directory_conflict`)"},"413":{"description":"Request body too large"},"422":{"description":"The repository has no branch named `production_branch` (code: `branch_not_found`), the project has no spicepod configuration (code: `spicepod_missing`), or the organization is not fully configured"},"500":{"description":"Internal server error"},"503":{"description":"GitHub did not answer (code: `github_unavailable`)"}},"tags":["Projects"]}},"/v1/projects/{projectId}/metrics":{"get":{"summary":"Get project metrics","description":"Returns current resource utilization metrics (CPU, memory, disk I/O) and data ingestion metrics for a project.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"in":"query","name":"window","required":false,"schema":{"type":"string"},"description":"Prometheus-style duration (e.g. `5m`, `1h`, `30s`). When provided, counter metrics are returned as rates over the window instead of raw cumulative values. For example, `rows_ingested` returns average rows per second with a window, or total cumulative rows without one."},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"Resource and ingestion metrics. Missing fields indicate no data is currently available. Individual metric queries may fail independently; partial results are returned when possible.","content":{"application/json":{"schema":{"type":"object","properties":{"ingestion":{"type":"object","description":"Aggregated data ingestion metrics","properties":{"rows_ingested":{"type":"number","description":"Total rows ingested (without window) or rows per second (with window)"},"bytes_ingested":{"type":"number","description":"Total bytes ingested (without window) or bytes per second (with window)"}}},"cluster":{"type":"object","description":"Cluster metrics for the project","properties":{"active_executors_count":{"type":"number","description":"Number of active executors registered with the scheduler"}}},"metrics":{"type":"object","description":"Resource metrics keyed by pod name","additionalProperties":{"type":"object","properties":{"cpu_usage_percent":{"type":"number","description":"CPU usage as a percentage (0-100)"},"memory_usage_bytes":{"type":"number","description":"Memory usage in bytes"},"disk_read_bytes":{"type":"number","description":"Disk bytes read per second (with window) or cumulative (without window)"},"disk_read_operations":{"type":"number","description":"Disk read operations per second (with window) or cumulative count (without window)"},"disk_write_bytes":{"type":"number","description":"Disk bytes written per second (with window) or cumulative (without window)"},"disk_write_operations":{"type":"number","description":"Disk write operations per second (with window) or cumulative count (without window)"}}}}}}}}},"400":{"description":"Invalid project ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"},"404":{"description":"Project or organization not found"},"422":{"description":"Organization is not fully configured"},"500":{"description":"Internal server error"},"503":{"description":"Metrics service unavailable"}},"tags":["Projects"]}},"/v1/projects/{projectId}/instances":{"get":{"summary":"List project instances","description":"Lists the runtime instances of a deployed project, newest first. A managed or BYOC project lists its Spicepod instances. A standalone project lists its Cloud Connect runtime. Read the logs of an instance with `GET /v1/projects/{projectId}/instances/{instanceName}/logs`.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"The project's instances","content":{"application/json":{"schema":{"type":"object","required":["instances"],"properties":{"instances":{"type":"array","items":{"$ref":"#/components/schemas/ProjectInstance"}}}}}}},"400":{"description":"Invalid project ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"},"404":{"description":"Project not found"},"409":{"description":"The project has no running instances. `code` is `project_not_deployed` when a managed or BYOC project was never deployed, or `project_paused` when the project is paused.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectRuntimeError"}}}},"422":{"description":"The project is not fully configured"},"500":{"description":"Internal server error"},"503":{"description":"The project runtime did not answer"}},"tags":["Projects"]}},"/v1/projects/{projectId}/instances/{instanceName}/logs":{"get":{"summary":"Get project instance logs","description":"Returns the most recent log lines of one instance of a deployed project, oldest first, without color codes. The org's plan sets the largest `tail`. List the instance names with `GET /v1/projects/{projectId}/instances`.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"in":"path","name":"instanceName","required":true,"schema":{"type":"string"},"description":"The instance name from `GET /v1/projects/{projectId}/instances`"},{"in":"query","name":"tail","required":false,"schema":{"type":"integer","minimum":1,"default":100},"description":"How many of the most recent lines to return"},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"The instance's most recent log lines","content":{"application/json":{"schema":{"type":"object","required":["instance","tail","lines"],"properties":{"instance":{"type":"string","description":"The instance name"},"tail":{"type":"integer","description":"The number of lines asked for"},"lines":{"type":"array","items":{"type":"string"},"description":"Log lines, oldest first"}}}}}},"400":{"description":"Invalid project ID or `tail`"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope (`code` is `insufficient_scope`), or `tail` is larger than the org's plan allows (`code` is `plan_limit`, and `max_tail` is the largest allowed `tail`).","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"properties":{"error":{"type":"string"},"code":{"type":"string"},"max_tail":{"type":"integer"}}}}}},"404":{"description":"The project was not found, or the instance is not one of the project's instances (`code` is `instance_not_found`)."},"409":{"description":"The logs cannot be read in the project's current state. `code` is `project_not_deployed` when the project was never deployed, `project_paused` when it is paused, or `instance_offline` when the Cloud Connect runtime is offline and does not answer.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"properties":{"error":{"type":"string"},"code":{"type":"string","enum":["project_not_deployed","project_paused","instance_offline"]}}}}}},"422":{"description":"The project is not fully configured"},"500":{"description":"Internal server error"},"503":{"description":"The project runtime did not answer"}},"tags":["Projects"]}},"/v1/projects/{projectId}/deployments":{"get":{"summary":"List deployments","description":"Returns a list of deployments for the specified project, ordered by most recent first.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"in":"query","name":"limit","schema":{"type":"integer","default":20},"description":"Maximum number of deployments to return"},{"in":"query","name":"status","schema":{"type":"string","enum":["queued","in_progress","succeeded","failed","created"]},"description":"Filter by deployment status"},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"List of deployments","content":{"application/json":{"schema":{"type":"object","properties":{"deployments":{"type":"array","items":{"$ref":"#/components/schemas/Deployment"}}}}}}},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"},"404":{"description":"Project not found"}},"tags":["Deployments"]},"post":{"summary":"Create a deployment","description":"Creates a new deployment for the specified project using its current spicepod configuration. A project connected to a GitHub repository deploys spicepod.yaml from its root directory, read when the deployment is created. It reads the head of the production branch, or the `branch` or `commit_sha` the request names. The runtime image is selected based on the project's update channel.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"$ref":"#/components/parameters/OrgName"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"image_tag":{"type":"string","description":"Override the runtime image tag for this deployment. Requires the Enterprise plan; omit it to deploy the image the project is configured with. When the effective update channel is `stable` or `preview`, a supplied tag must be a published version for that channel. When the effective update channel is `nightly`, a supplied tag must be a published `spicehq/spiceai` `nightly-*` tag (`YYYYMMDDHHmm` plus `-` plus 7 to 40 lowercase hex characters, for example `202609081124-cadf292`)."},"channel":{"type":"string","enum":["stable","preview","nightly"],"description":"Update channel that determines the runtime image"},"expected_update_channel":{"type":"string","enum":["stable","preview","nightly"],"description":"If set, the stored project update channel must match this value or the request is refused with 409 `update_channel_mismatch`. This is a precondition, not an override (`channel` still changes the image train).\n"},"replicas":{"type":"integer","minimum":1,"maximum":10,"description":"Override the number of replicas for this deployment"},"branch":{"type":"string","minLength":1,"maxLength":255,"description":"For a project connected to a GitHub repository, deploy spicepod.yaml from the head of this branch or tag of that repository instead of the production branch. The branch or tag must exist. With `commit_sha`, it must contain the commit. The production branch and the stored spicepod do not change, so the next deployment without a revision, and any automatic redeploy, deploy the production branch head again. A project with no connected repository refuses this field with 400 `repository_not_connected`. A project whose org has no GitHub App installation refuses it with 422 `github_installation_missing`. A request from the deploy GitHub Action (User-Agent `spice-cloud-deploy-action`) does not check or use this field for any project, because the Action sends the branch of its workflow run as metadata.\n"},"commit_sha":{"type":"string","pattern":"^[0-9a-fA-F]{7,40}$","description":"For a project connected to a GitHub repository, deploy spicepod.yaml from this commit instead of the head of the production branch. The commit must be on `branch`, or on the production branch (never a tag of the same name) when `branch` is omitted, so a commit that only a fork holds is refused. The deployment records the full SHA. The production branch and the stored spicepod do not change. A project with no connected repository refuses this field with 400 `repository_not_connected`. A project whose org has no GitHub App installation refuses it with 422 `github_installation_missing`. A request from the deploy GitHub Action (User-Agent `spice-cloud-deploy-action`) does not check or use this field for any project, because the Action sends the commit of its workflow run as metadata.\n"},"commit_message":{"type":"string","description":"Not used. A deployment from a connected repository records the message of the commit it reads.\n"},"debug":{"type":"boolean","description":"Enable debug mode for this deployment"},"target":{"type":"object","description":"Optional per-deployment override. Omit (or `{ kind: cloud }`) to place the deployment where the project itself runs: Spice-managed compute, its own BYOC cluster, or its attached standalone Cloud Connect instance. That placement is a property of the project, not of this request. Pass `{ kind: cluster, instance_id }` to override that for this deployment and route it to a BYOC cluster connection; the cluster must be owned by the token's org, not in a terminal lifecycle state (decommissioning/archived), not paused, and the operator must hold a live connection (verified by the connectivity ping, not the Supabase lifecycle status). A standalone project cannot be overridden this way and is refused with 409.\n","properties":{"kind":{"type":"string","enum":["cloud","cluster"]},"instance_id":{"type":"string","description":"Required when kind is `cluster`."}}}}}}}},"responses":{"202":{"description":"Deployment created and queued for processing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Deployment"}}}},"400":{"description":"Invalid request body, project has no spicepod configuration, the spicepod has been deleted, or the spicepod is paused (`code: spicepod_paused`; resume it with POST /v1/projects/{projectId}/resume before deploying). Also returned when the spicepod `name`, `kind` or `version` is missing or not valid: the body is `{ error: 'Invalid spicepod configuration' }` with an `issues` array of `{path, message}`. The runtime checks all other spicepod fields when it loads the spicepod. For a standalone Cloud Connect project, also returned when the Spicepod references a `${secrets:...}` name the project does not define: the body carries `code: unknown_secret_references`, a `missing_secrets` array of `{name, path, suggestion?}`, and `secrets_url`. When the effective update channel is `stable` or `preview` and `image_tag` is supplied, the tag must be a published version for that channel or the body is `{ error: 'Invalid image_tag for stable channel', code: 'invalid_stable_image_tag' }` (or the preview analog). When the effective update channel is `nightly` and `image_tag` is supplied, the tag must be a published `spicehq/spiceai` `nightly-*` tag of the form `YYYYMMDDHHmm` plus `-` plus 7 to 40 lowercase hex characters (for example `202609081124-cadf292`) or the body is `{ error: 'Invalid image_tag for nightly channel', code: 'invalid_nightly_image_tag' }`. For a project connected to a GitHub repository, also returned when the root directory has no spicepod.yaml or it is invalid or too large (`code: spicepod_not_found`, `spicepod_invalid` or `spicepod_too_large`), when `commit_sha` is not on `branch` (or on the production branch when `branch` is omitted) (`code: commit_not_on_branch`), or when `commit_sha` comes without `branch` and the project has no production branch (`code: branch_required`). Nothing is dispatched and no deployment is recorded. Also returned when `branch` or `commit_sha` is sent for a project with no connected repository (`code: repository_not_connected`), except from the deploy GitHub Action. Nothing is dispatched.\n"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope, viewer role cannot create deployments, `image_tag` was supplied outside the Enterprise plan (code: `image_tag_requires_enterprise`), the requested runtime image is not available for the organization plan (code: `image_tag_not_allowed_for_plan`), or the organization's GitHub App installation cannot read the connected repository (code: `github_unauthorized` or `github_forbidden`), was removed (code: `github_installation_removed`), is suspended (code: `github_installation_suspended`), or has no access to the connected repository (code: `github_repository_unreachable`); omitting `image_tag` deploys the image the project is configured with"},"404":{"description":"Project not found. For a `cluster` target, also returned when the BYOC cluster connection does not exist or is not owned by the token's org. Also returned when the connected GitHub repository, its production branch, or the requested `branch` or `commit_sha` was not found (`code: github_not_found`). With `commit_sha` and no `branch`, this includes a production branch that no longer exists.\n"},"409":{"description":"A deployment is already in progress for this project (`code: deployment_in_progress`, with optional `activeDeploymentId`). For a `cluster` target, also returned when the BYOC cluster is paused, offline, or otherwise not in a deployable state. Also returned when the project's deploy target is unusable: its kind conflicts with its cluster/instance references, a `cluster` target was requested for a standalone project, a standalone project has no attached instance, or that instance has not yet enrolled the encryption key its secrets are sealed to (retrying once it is online succeeds). Also returned when `expected_update_channel` does not match the stored spicepod update channel, or when that channel changes before dispatch (`code: update_channel_mismatch`). Also returned when the project is paused after the request checked it (`code: spicepod_paused`), or its spicepod is deleted after that check (`code: spicepod_deleted`). Also returned when the project's repository, root directory, or production branch changes while the spicepod is read from the repository; deploy again. Nothing is dispatched and no deployment is recorded.\n"},"413":{"description":"Request body exceeds the maximum size (1 MiB), or, for a standalone Cloud Connect project, the project's resolved secrets exceed the maximum sealed-payload size. Nothing is dispatched and no deployment is recorded.\n"},"422":{"description":"Organization is not fully configured, no runtime version in the catalog matches this project’s version range (`code: version_no_match`), or the spicepod uses version v1beta1, which the Spice 2 runtime cannot load (`code: spicepod_version_unsupported`). Also returned when the Spicepod uses a connection credential that holds a secret reference deploy cannot resolve: the body carries `code: connection_credentials_unresolved` and an `unresolved_credentials` array of `{connection, field}`. Nothing is deployed. Also returned when `branch` or `commit_sha` is sent for a project with a connected repository whose org has no GitHub App installation to read it (`code: github_installation_missing`), except from the deploy GitHub Action. Nothing is dispatched.\n"},"429":{"description":"GitHub rate-limited the read of the connected repository (`code: github_rate_limited`).\n"},"500":{"description":"Internal server error. For a standalone Cloud Connect project, also returned with `code: deployment_not_settled` when the deployment record could not be moved out of `queued`; the project's next deployment is refused until it is cleared.\n"},"502":{"description":"Upstream service failure while provisioning the deployment, or, for a standalone Cloud Connect project, the spicepod could not be handed to the instance's gateway, or the instance rejected it. Also returned when GitHub did not answer while the connected repository was read (`code: github_unavailable`).\n"},"503":{"description":"The runtime version catalog is unavailable or empty (`code: version_source_unavailable`), so an unpinned stable or preview deploy cannot resolve an image tag, or catalog membership for an explicit `image_tag` override cannot be validated. Also returned when the published nightly tag list cannot be loaded to validate a nightly `image_tag` override.\n"}},"tags":["Deployments"]}},"/v1/projects/{projectId}/deployments/{deploymentId}":{"get":{"summary":"Get a deployment","description":"Returns a single deployment for the specified project. The body matches one item from GET /v1/projects/{projectId}/deployments. Not cacheable. Poll this path for stored status after POST create.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"in":"path","name":"deploymentId","required":true,"schema":{"type":"integer"},"description":"The ID of the deployment"},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"Deployment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Deployment"}}}},"400":{"description":"Invalid project ID or deployment ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"},"404":{"description":"Project or deployment not found"},"500":{"description":"Internal server error"}},"tags":["Deployments"]}},"/v1/projects/{projectId}/secrets":{"get":{"summary":"List secrets","description":"Returns all secrets for the specified project. Secret values are always masked.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"List of secrets","content":{"application/json":{"schema":{"type":"object","properties":{"secrets":{"type":"array","items":{"$ref":"#/components/schemas/Secret"}}}}}}},"400":{"description":"Invalid project ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope or viewer role cannot perform write operations"},"404":{"description":"Project not found"},"500":{"description":"Internal server error"}},"tags":["Secrets"]},"post":{"summary":"Create or update a secret","description":"Creates a new secret or updates an existing secret with the same name. The secret value is encrypted at rest.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"$ref":"#/components/parameters/OrgName"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","value"],"properties":{"name":{"type":"string","pattern":"^[a-zA-Z_][a-zA-Z0-9_]*$","description":"Secret name (must start with a letter or underscore, alphanumeric and underscores only)"},"value":{"type":"string","description":"Secret value (will be encrypted at rest)"}}}}}},"responses":{"200":{"description":"Secret created or updated successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Secret"}}}},"400":{"description":"Invalid request body or project ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"},"404":{"description":"Project not found"},"500":{"description":"Internal server error"}},"tags":["Secrets"]}},"/v1/projects/{projectId}/secrets/{secretName}":{"get":{"summary":"Get a secret","description":"Returns a specific secret by name. The secret value is always masked.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"in":"path","name":"secretName","required":true,"schema":{"type":"string"},"description":"The name of the secret"},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"Secret details (value is masked)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Secret"}}}},"400":{"description":"Invalid project ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope or admin or owner role required"},"404":{"description":"Project or secret not found"},"500":{"description":"Internal server error"}},"tags":["Secrets"]},"delete":{"summary":"Delete a secret","description":"Permanently deletes a secret by name.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"in":"path","name":"secretName","required":true,"schema":{"type":"string"},"description":"The name of the secret"},{"$ref":"#/components/parameters/OrgName"}],"responses":{"204":{"description":"Secret deleted successfully"},"400":{"description":"Invalid project ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"},"404":{"description":"Project or secret not found"},"500":{"description":"Internal server error"}},"tags":["Secrets"]}},"/v1/projects/{projectId}/org-secrets":{"get":{"summary":"List the organization secrets linked to a project","description":"Returns the organization secrets this project receives at deploy time. Values are always masked. `shadowed` is true when a project secret has the same name; the project secret is the one delivered.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"The linked organization secrets, masked","content":{"application/json":{"schema":{"type":"object","properties":{"org_secrets":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/Secret"},{"type":"object","properties":{"shadowed":{"type":"boolean"}}}]}}}}}}},"400":{"description":"Invalid project ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope (requires secrets:read) or the viewer role"},"404":{"description":"Project not found"},"500":{"description":"Internal server error"}},"tags":["Secrets"]}},"/v1/projects/{projectId}/org-secrets/{secretName}":{"put":{"summary":"Link an organization secret to a project","description":"Makes the named organization secret available to the project at deploy time. Linking a secret that is already linked changes nothing. The project's own secrets are not changed; a project secret with the same name takes precedence.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"in":"path","name":"secretName","required":true,"schema":{"type":"string"},"description":"The name of the organization secret"},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"The secret was already linked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Secret"}}}},"201":{"description":"The secret was linked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Secret"}}}},"400":{"description":"Invalid project ID or secret name"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope (requires secrets:write) or the viewer role"},"404":{"description":"Project or organization secret not found"},"409":{"description":"The project already links the maximum of 1000 organization secrets (code: `org_secret_link_limit`)"},"429":{"description":"Another link change is in progress for this project. Try again."},"500":{"description":"Internal server error"},"503":{"description":"Failed to link the organization secret. Try again."}},"tags":["Secrets"]},"delete":{"summary":"Unlink an organization secret from a project","description":"Stops delivering the named organization secret to the project from its next deploy. The secret itself is kept.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"in":"path","name":"secretName","required":true,"schema":{"type":"string"},"description":"The name of the organization secret"},{"$ref":"#/components/parameters/OrgName"}],"responses":{"204":{"description":"The secret was unlinked"},"400":{"description":"Invalid project ID or secret name"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope (requires secrets:write) or the viewer role"},"404":{"description":"Project or organization secret not found, or the secret is not linked to the project"},"429":{"description":"Another link change is in progress for this project. Try again."},"500":{"description":"Internal server error"},"503":{"description":"Failed to unlink the organization secret. Try again."}},"tags":["Secrets"]}},"/v1/projects/{projectId}/api-keys":{"get":{"summary":"Get API keys","description":"Returns the API keys for a specific project. API keys are used to authenticate requests to the Spice.ai runtime.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"API keys for the project","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeys"}}}},"400":{"description":"Invalid project ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope or viewer role cannot regenerate API keys"},"404":{"description":"Project not found"},"500":{"description":"Internal server error"}},"tags":["API Keys"]},"post":{"summary":"Regenerate an API key","description":"Regenerates an API key for the specified project. This invalidates the previous key. Use key_number to specify which key to regenerate (0 for both, 1 for primary, 2 for secondary).","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"$ref":"#/components/parameters/OrgName"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"key_number":{"type":"integer","enum":[0,1,2],"default":1,"description":"Which API key to regenerate (0 = both, 1 = primary, 2 = secondary)"}}}}}},"responses":{"200":{"description":"API key regenerated successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeysRegenerated"}}}},"400":{"description":"Invalid request body or project ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"},"404":{"description":"Project not found"},"500":{"description":"Internal server error"}},"tags":["API Keys"]}},"/v1/projects/{projectId}/monitors":{"get":{"summary":"List project monitors","description":"Returns the project's monitors. Requires `monitors:read`. Monitors on a project that is not managed, or on a template that is not available to the organization, are omitted. An existing `container_cpu` monitor without an effective CPU limit remains visible with `evaluation_unavailable_reason` set to `cpu_limit_missing`.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"List of project monitors","content":{"application/json":{"schema":{"type":"object","properties":{"monitors":{"type":"array","items":{"$ref":"#/components/schemas/ProjectMonitor"}}}}}}},"400":{"description":"Invalid project ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"},"404":{"description":"Project not found"},"500":{"description":"Internal server error"}},"tags":["Monitors"]},"post":{"summary":"Create a project monitor","description":"Creates a project monitor from a runtime template. Requires `monitors:write`. A monitor evaluates a runtime metric on a schedule and fires when the comparison holds; to fire on matching dataset rows instead, create a data reaction at `/v1/projects/{projectId}/reactions`.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"$ref":"#/components/parameters/OrgName"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","templateId","spec"],"properties":{"name":{"type":"string","description":"Display name. Unique, case-insensitively, among the project's live monitors and data reactions. A collision answers 409.","minLength":1,"maxLength":100},"description":{"type":"string","maxLength":500},"kind":{"type":"string","description":"Optional assertion that `templateId` names a monitor. A value that disagrees with the template is refused rather than ignored; omit it and the template decides.","enum":["runtime"]},"templateId":{"type":"string","description":"The runtime template the monitor evaluates. `query_latency_p95` is retained for existing monitors but cannot be used to create new ones; use `query_latency_p99`. `container_cpu` is available only on managed projects with a defined CPU limit. A data-reaction template is refused here. Create those at `/v1/projects/{projectId}/reactions`.","enum":["query_failures","query_latency_p95","query_latency_p99","llm_failures","dataset_refresh_errors","http_5xx","flight_failures","memory_working_set","container_cpu","dataset_status","instance_health"]},"spec":{"type":"object","description":"Monitor parameters. Which properties apply is decided by `templateId`. Each property below names the templates that read it, and a property no monitor template reads is stored rather than rejected. `spec` is shared with data reactions, so it accepts their properties too; `model` is the one that is refused rather than stored.","required":["op","threshold"],"properties":{"op":{"type":"string","description":"Comparison applied to the metric. Required on every monitor spec. `instance_health` requires `GT`.","enum":["GT","GEQ","LT","LEQ","EQ","NEQ"]},"threshold":{"type":"number","description":"Value compared against the metric. HTTP failure monitors count estimated 5xx responses in the selected window. Rate-based conditions use responses per second. `dataset_refresh_errors` counts refresh errors in the window. `query_latency_p99` uses milliseconds. `memory_working_set` uses percent of the instance memory limit. A stored memory threshold above 1000 uses bytes. `container_cpu` uses percent of the defined CPU limit. `dataset_status` compares load-state numbers. `instance_health` requires 0. Every monitor spec requires a threshold."},"window":{"type":"string","description":"Range evaluated by `query_failures`, `query_latency_p99`, `llm_failures`, `dataset_refresh_errors`, `http_5xx`, `flight_failures`, and `container_cpu`. `memory_working_set` and `dataset_status` read the current value and ignore this field. `instance_health` reads current failure states and uses a fixed five-minute eviction observation window, independent of this field. Data reactions store this field but do not use it.","default":"5m","enum":["1m","5m","15m","30m","1h"]},"sustainSecs":{"type":"integer","description":"Sustain delay in seconds. Failure monitors default to 0. Other monitors default to 300. Explicit values are preserved.","minimum":0,"maximum":86400},"severity":{"type":"string","description":"Severity the monitor fires at. Recorded on each monitor event and carried into the email or webhook notification.","default":"critical","enum":["warn","critical"]},"dataset":{"type":"string","description":"Read by `dataset_status`, which watches this one dataset. Omit it to match any dataset in the project.","minLength":1,"maxLength":128,"pattern":"^(?!.*\\.\\.)[A-Za-z_][A-Za-z0-9_.-]*$"},"includeDetails":{"type":"boolean","description":"When true, the recorded event and the notification may carry a `details` string quoted from the firing payload when it contains useful information. Off by default, because that payload may be sensitive. A notification's own summary, description, and observed value do not depend on it."},"value":{"description":"Read by `dataset_status` for notification wording only, and only when `threshold` is not one of the state numbers above and this names a state. It never changes what the monitor evaluates. Otherwise a data-reaction property.","oneOf":[{"type":"string","maxLength":256},{"type":"number"}]},"column":{"type":"string","description":"A data-reaction property that no monitor evaluates. Leave it unset. A monitor carrying both `column` and `dataset` has its notification describe that row predicate instead of its own metric comparison.","minLength":1,"maxLength":64,"pattern":"^[A-Za-z_][A-Za-z0-9_]*$"},"datasets":{"type":"array","description":"A data-reaction property that no monitor reads.","maxItems":20,"items":{"type":"string","minLength":1,"maxLength":128,"pattern":"^(?!.*\\.\\.)[A-Za-z_][A-Za-z0-9_.-]*$"}},"conditions":{"type":"array","description":"A data-reaction property that no monitor evaluates. Leave it unset. A monitor carrying conditions has its notification describe them instead of its own metric comparison. IS_NULL and IS_NOT_NULL omit value.","maxItems":20,"items":{"type":"object","required":["dataset","column","op"],"properties":{"dataset":{"type":"string","minLength":1,"maxLength":128,"pattern":"^(?!.*\\.\\.)[A-Za-z_][A-Za-z0-9_.-]*$"},"column":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[A-Za-z_][A-Za-z0-9_]*$"},"op":{"type":"string","enum":["EQ","NEQ","GT","GEQ","LT","LEQ","CONTAINS","STARTS_WITH","ENDS_WITH","IS_NULL","IS_NOT_NULL"]},"value":{"description":"Required except for IS_NULL and IS_NOT_NULL.","oneOf":[{"type":"string","minLength":1,"maxLength":256},{"type":"number"}]}}}},"returnFields":{"$ref":"#/components/schemas/ReactionReturnFields"},"queryLanguage":{"type":"string","description":"A data-reaction property that no monitor reads.","enum":["cypher","gql"]},"query":{"type":"string","description":"A data-reaction property that no monitor reads.","maxLength":10000},"model":{"type":"object","description":"Model transform. Data reactions only. Sending it with a monitor template is refused with 400, not ignored.","required":["model","instructions"],"properties":{"model":{"type":"string","minLength":1,"maxLength":128},"instructions":{"type":"string","minLength":1,"maxLength":4000}}}}},"target":{"description":"Notification sink. Omit it to email the user the credential belongs to; a machine credential has no user, so its monitors email the organization owner.","$ref":"#/components/schemas/ClusterMonitorTarget"},"targets":{"type":"array","minItems":1,"maxItems":3,"description":"Full destination set. When present, this replaces `target` and is the complete list. At most one sink of each type.","items":{"$ref":"#/components/schemas/ClusterMonitorTarget"}},"recipientUserIds":{"type":"array","description":"Legacy spelling of `target.recipientUserIds`, read only when an email target omits its own.","maxItems":50,"items":{"type":"integer","minimum":1}}}}}}},"responses":{"201":{"description":"Monitor created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectMonitor"}}}},"400":{"description":"Invalid request body or project ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"},"404":{"description":"Project not found, or the template is not available for this project (`code` is `monitor_template_unavailable`). Monitors run only on managed projects, and only released templates are available unless the organization has early access."},"409":{"description":"A live monitor or data reaction in this project already has this name"},"413":{"description":"Request body too large"},"422":{"description":"Slack is not connected for the organization"},"500":{"description":"Internal server error"},"502":{"description":"Failed to provision monitor"}},"tags":["Monitors"]}},"/v1/projects/{projectId}/monitors/{alertId}":{"get":{"summary":"Get a project monitor","description":"Returns a single project monitor. Requires `monitors:read`. An existing `container_cpu` monitor without an effective CPU limit remains visible with `evaluation_unavailable_reason` set to `cpu_limit_missing`.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"}},{"in":"path","name":"alertId","required":true,"schema":{"type":"string","format":"uuid"}},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"Monitor","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectMonitor"}}}},"400":{"description":"Invalid project or monitor ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"},"404":{"description":"Project or monitor not found"},"500":{"description":"Internal server error"}},"tags":["Monitors"]},"patch":{"summary":"Update a project monitor","description":"Updates a project monitor. Requires `monitors:write` and organization admin privileges.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"}},{"in":"path","name":"alertId","required":true,"schema":{"type":"string","format":"uuid"}},{"$ref":"#/components/parameters/OrgName"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"description":{"type":["string","null"],"maxLength":500},"status":{"type":"string","enum":["active","disabled"]},"templateId":{"type":"string"},"spec":{"type":"object"},"target":{"description":"Omit to keep the existing destination.","$ref":"#/components/schemas/ClusterMonitorTarget"},"recipientUserIds":{"type":"array","items":{"type":"integer"}}}}}}},"responses":{"200":{"description":"Monitor updated"},"400":{"description":"Invalid request or monitor ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope or role"},"404":{"description":"Project or monitor not found"},"409":{"description":"Duplicate monitor name or monitor changed during update"},"413":{"description":"Request body too large"},"422":{"description":"Destination is unavailable or project configuration is incomplete"},"500":{"description":"Internal server error"},"502":{"description":"Monitor backend update failed"}},"tags":["Monitors"]},"delete":{"summary":"Delete a project monitor","description":"Soft-deletes a project monitor and tears down the monitor it provisioned. Requires `monitors:write`.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"}},{"in":"path","name":"alertId","required":true,"schema":{"type":"string","format":"uuid"}},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"Monitor deleted"},"400":{"description":"Invalid project or monitor ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"},"404":{"description":"Project or monitor not found"},"500":{"description":"Internal server error"},"502":{"description":"Failed to delete monitor"}},"tags":["Monitors"]}},"/v1/projects/{projectId}/reactions":{"get":{"summary":"List data reactions","description":"Returns Drasi data reactions for the specified project. Requires `reactions:read`.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"List of data reactions","content":{"application/json":{"schema":{"type":"object","properties":{"reactions":{"type":"array","items":{"$ref":"#/components/schemas/Reaction"}}}}}}},"400":{"description":"Invalid project ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"},"404":{"description":"Project not found"},"500":{"description":"Internal server error"}},"tags":["Reactions"]},"post":{"summary":"Create a data reaction","description":"Creates a Drasi data reaction from a data template. Requires `reactions:write`. Email and HTTP are sinks. Optional `spec.model` transforms the matching query result with a spicepod model that can tool-call back to Cloud to send the configured sink.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"$ref":"#/components/parameters/OrgName"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","templateId","spec"],"properties":{"name":{"type":"string","description":"Display name. Unique, case-insensitively, among the project's live monitors and data reactions. A collision answers 409."},"description":{"type":"string"},"templateId":{"type":"string"},"spec":{"type":"object","description":"Reaction parameters. Which properties apply is decided by `templateId`. Each property below names the templates that read it, and a property a template does not read is accepted and ignored rather than rejected.","required":["op","threshold"],"properties":{"op":{"type":"string","description":"Comparison operator. Required on every reaction spec.","enum":["GT","GEQ","LT","LEQ","EQ","NEQ"]},"threshold":{"type":"number","description":"Numeric threshold. Required on every reaction spec. `task_history_slow` compares it against the task duration in milliseconds; `dataset_row_match` and `dataset_query` match on the query itself, so they conventionally send 0."},"window":{"type":"string","description":"Evaluation window. Only project monitors (`/v1/projects/{projectId}/monitors`) act on it. A data reaction fires on the matching row, so the value is stored and never read.","default":"5m","enum":["1m","5m","15m","30m","1h"]},"sustainSecs":{"type":"integer","description":"How long the condition must hold before firing, in seconds. Like `window`, only project monitors act on it.","default":300,"minimum":0,"maximum":86400},"severity":{"type":"string","description":"Severity recorded on each reaction event and carried into the email or webhook notification.","default":"critical","enum":["warn","critical"]},"dataset":{"type":"string","description":"Single dataset to watch. Required by `dataset_row_match` when `conditions` is omitted.","maxLength":128},"column":{"type":"string","description":"Column to compare. Required by `dataset_row_match` when `conditions` is omitted.","maxLength":64},"value":{"description":"Value to compare `column` against. Required by `dataset_row_match` when `conditions` is omitted.","oneOf":[{"type":"string","maxLength":256},{"type":"number"}]},"datasets":{"type":"array","description":"Datasets the reaction watches. `dataset_row_match` uses this list. Omit it to derive names from `conditions`. `dataset_query` does not read this field.","maxItems":20,"items":{"type":"string","maxLength":128}},"conditions":{"type":"array","description":"Row-match predicates, read by `dataset_row_match`. CONTAINS, STARTS_WITH, ENDS_WITH, IS_NULL, and IS_NOT_NULL are allowed in addition to equality and ordering. IS_NULL and IS_NOT_NULL omit value.","maxItems":20,"items":{"type":"object","required":["dataset","column","op"],"properties":{"dataset":{"type":"string"},"column":{"type":"string"},"op":{"type":"string","enum":["EQ","NEQ","GT","GEQ","LT","LEQ","CONTAINS","STARTS_WITH","ENDS_WITH","IS_NULL","IS_NOT_NULL"]},"value":{"description":"Required except for IS_NULL and IS_NOT_NULL.","oneOf":[{"type":"string"},{"type":"number"}]}}}},"returnFields":{"$ref":"#/components/schemas/ReactionReturnFields"},"queryLanguage":{"type":"string","description":"Language of `query`, read by `dataset_query`. Defaults to `gql` when omitted.","default":"gql","enum":["cypher","gql"]},"query":{"type":"string","description":"Authored continuous query. Required by `dataset_query`.","maxLength":10000},"includeDetails":{"type":"boolean","description":"When true, notifications include matching row details. Off by default."},"model":{"type":"object","description":"Optional spicepod model transform (data reactions only). The model can call one tool for each selected sink (send_email, send_webhook, or send_slack).","required":["model","instructions"],"properties":{"model":{"type":"string","description":"Spicepod model or worker name"},"instructions":{"type":"string","description":"System instructions sent with each matching query result"}}}}},"target":{"description":"Reaction sink. Omit to email the caller.","$ref":"#/components/schemas/ClusterMonitorTarget"},"targets":{"type":"array","minItems":1,"maxItems":3,"description":"Full destination set. When present, this replaces `target` and is the complete list. At most one sink of each type.","items":{"$ref":"#/components/schemas/ClusterMonitorTarget"}}}}}}},"responses":{"201":{"description":"Reaction created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Reaction"}}}},"400":{"description":"Invalid request body or project ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"},"404":{"description":"Project not found"},"409":{"description":"A live monitor or data reaction in this project already has this name"},"413":{"description":"Request body too large"},"422":{"description":"A required project capability or Slack connection is unavailable"},"500":{"description":"Internal server error"},"502":{"description":"Failed to provision reaction"},"503":{"description":"The project's Drasi data source is starting or unavailable"}},"tags":["Reactions"]}},"/v1/projects/{projectId}/reactions/{alertId}":{"get":{"summary":"Get a data reaction","description":"Returns a single data reaction. Requires `reactions:read`.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"}},{"in":"path","name":"alertId","required":true,"schema":{"type":"string","format":"uuid"}},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"Reaction","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Reaction"}}}},"400":{"description":"Invalid project or reaction ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"},"404":{"description":"Project or reaction not found"},"500":{"description":"Internal server error"}},"tags":["Reactions"]},"patch":{"summary":"Update a data reaction","description":"Updates reaction fields supplied in the request. Requires `reactions:write` and organization membership.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"}},{"in":"path","name":"alertId","required":true,"schema":{"type":"string","format":"uuid"}},{"$ref":"#/components/parameters/OrgName"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"description":{"type":["string","null"],"maxLength":500},"status":{"type":"string","enum":["active","disabled"]},"templateId":{"type":"string"},"spec":{"type":"object"},"target":{"$ref":"#/components/schemas/ClusterMonitorTarget"},"targets":{"type":"array","minItems":1,"maxItems":3,"items":{"$ref":"#/components/schemas/ClusterMonitorTarget"}},"recipientUserIds":{"type":"array","items":{"type":"integer"}}}}}}},"responses":{"200":{"description":"Reaction updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Reaction"}}}},"400":{"description":"Invalid request, specification, or dataset"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope or organization membership"},"404":{"description":"Project or reaction not found"},"409":{"description":"Name conflict or concurrent update"},"413":{"description":"Request body too large"},"422":{"description":"Project configuration is incomplete"},"429":{"description":"Another update is in progress for this reaction"},"500":{"description":"Internal server error"},"502":{"description":"Drasi update failed"},"503":{"description":"The project's Drasi data source or lock store is unavailable"}},"tags":["Reactions"]},"delete":{"summary":"Delete a data reaction","description":"Soft-deletes a data reaction and tears down its Drasi query. Requires `reactions:write`.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"}},{"in":"path","name":"alertId","required":true,"schema":{"type":"string","format":"uuid"}},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"Reaction deleted"},"400":{"description":"Invalid project or reaction ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"},"404":{"description":"Project or reaction not found"},"500":{"description":"Internal server error"},"502":{"description":"Failed to delete reaction"}},"tags":["Reactions"]}},"/v1/clusters/{clusterId}/monitors":{"get":{"summary":"List cluster monitors","description":"Lists live monitors for the cluster. Requires `monitors:read` and organization membership. The cluster ID is the `cluster_name` returned by GET /v1/clusters.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"clusterId","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"Cluster monitors"},"400":{"description":"Invalid X-Org-Name header"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope or role"},"404":{"description":"Cluster not found"},"500":{"description":"Internal server error"}},"tags":["Clusters"]},"post":{"summary":"Create a cluster monitor","description":"Creates and provisions a cluster monitor. Requires `monitors:write` and organization admin privileges.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"clusterId","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/OrgName"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","templateId","spec"],"properties":{"name":{"type":"string","minLength":1,"maxLength":100},"description":{"type":"string","maxLength":500},"templateId":{"type":"string","enum":["cluster_cpu","cluster_memory","cluster_availability"]},"spec":{"$ref":"#/components/schemas/ClusterMonitorSpec"},"target":{"description":"Omit to email the caller, or the organization owner for a machine credential.","$ref":"#/components/schemas/ClusterMonitorTarget"},"targets":{"type":"array","minItems":1,"maxItems":3,"description":"Full destination set. When present, this replaces `target`. At most one sink of each type.","items":{"$ref":"#/components/schemas/ClusterMonitorTarget"}},"recipientUserIds":{"type":"array","items":{"type":"integer"}}}}}}},"responses":{"201":{"description":"Monitor created"},"400":{"description":"Invalid request body or monitor configuration, or the cluster already has 20 live monitors"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope or role"},"404":{"description":"Cluster not found"},"409":{"description":"Duplicate monitor name or monitor deleted during creation","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"properties":{"error":{"type":"string"},"code":{"type":"string","enum":["monitor_name_conflict","monitor_deleted_during_creation"]}}}}}},"413":{"description":"Request body too large"},"422":{"description":"Destination is unavailable"},"500":{"description":"Internal server error"},"502":{"description":"Monitor provisioning or cleanup failed. If cleanup is pending, the response includes the monitor ID for a retry with DELETE."}},"tags":["Clusters"]}},"/v1/clusters/{clusterId}/monitors/{alertId}":{"get":{"summary":"Get a cluster monitor","description":"Returns a live cluster monitor. Requires `monitors:read` and organization membership.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"clusterId","required":true,"schema":{"type":"string"}},{"in":"path","name":"alertId","required":true,"schema":{"type":"string","format":"uuid"}},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"Monitor"},"400":{"description":"Invalid monitor ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope or role"},"404":{"description":"Cluster or monitor not found"},"500":{"description":"Internal server error"}},"tags":["Clusters"]},"patch":{"summary":"Update a cluster monitor","description":"Updates the rule, destination, or status and reprovisions the monitor. Requires `monitors:write` and organization admin privileges.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"clusterId","required":true,"schema":{"type":"string"}},{"in":"path","name":"alertId","required":true,"schema":{"type":"string","format":"uuid"}},{"$ref":"#/components/parameters/OrgName"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"description":{"type":["string","null"],"maxLength":500},"status":{"type":"string","enum":["active","disabled"]},"templateId":{"type":"string","enum":["cluster_cpu","cluster_memory","cluster_availability"]},"spec":{"$ref":"#/components/schemas/ClusterMonitorSpec"},"target":{"description":"Omit to keep the existing destination.","$ref":"#/components/schemas/ClusterMonitorTarget"},"targets":{"type":"array","minItems":1,"maxItems":3,"description":"Full destination set. When present, replaces the stored sinks. Omit to keep them.","items":{"$ref":"#/components/schemas/ClusterMonitorTarget"}},"recipientUserIds":{"type":"array","items":{"type":"integer"}}}}}}},"responses":{"200":{"description":"Monitor updated"},"400":{"description":"Invalid request or monitor ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope or role"},"404":{"description":"Cluster or monitor not found"},"409":{"description":"Duplicate monitor name or monitor changed during update","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"properties":{"error":{"type":"string"},"code":{"type":"string","enum":["monitor_name_conflict","monitor_changed"]}}}}}},"413":{"description":"Request body too large"},"422":{"description":"Destination is unavailable"},"500":{"description":"Internal server error"},"502":{"description":"Monitor backend update failed; the saved rule may be pending and the request can be retried"}},"tags":["Clusters"]},"delete":{"summary":"Delete a cluster monitor","description":"Soft-deletes the monitor and retires its backend. Requires `monitors:write` and organization admin privileges. Retry to finish pending cleanup.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"clusterId","required":true,"schema":{"type":"string"}},{"in":"path","name":"alertId","required":true,"schema":{"type":"string","format":"uuid"}},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"Monitor deleted"},"400":{"description":"Invalid monitor ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope or role"},"404":{"description":"Cluster or monitor not found"},"500":{"description":"Internal server error"},"502":{"description":"Backend cleanup failed"}},"tags":["Clusters"]}},"/v1/orgs":{"get":{"summary":"List the caller's organizations","description":"Returns every organization the authenticated caller belongs to, each with the caller's effective membership role. A caller with no memberships gets an empty array, not a 404, because clients read 404 as \"this deployment does not serve the endpoint\", which is a different state. `name` is the organization handle used in `<org>/<app>` addressing.\n","security":[{"BearerAuth":[]}],"parameters":[{"in":"header","name":"X-Org-Name","required":false,"schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"},"description":"Optional for every credential, a portal session token included: this listing is how a session finds the organizations it can name elsewhere. When sent, it is authorized as on any other route, so a handle the caller does not belong to is `org_forbidden`.\n"}],"responses":{"200":{"description":"The organizations the caller is a member of, possibly empty","content":{"application/json":{"schema":{"type":"object","required":["orgs"],"properties":{"orgs":{"type":"array","items":{"type":"object","required":["id","name","role"],"properties":{"id":{"type":"integer"},"name":{"type":"string"},"role":{"type":"string","enum":["owner","admin","member","viewer"],"description":"The caller's highest-privilege role in the organization."}}}}}}}}},"400":{"description":"X-Org-Name is not a valid organization handle (`invalid_org_assertion`)"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope (`insufficient_scope`), or X-Org-Name names an organization the caller cannot act for (`org_forbidden`, `org_assertion_mismatch`, `org_deleted`)\n"},"500":{"description":"Internal server error"}},"tags":["Orgs"]}},"/v1/orgs/{orgId}/secrets":{"get":{"summary":"List organization secrets","description":"Returns the organization's secrets. Values are always masked. Platform-managed secrets are not listed. An organization secret reaches a project only through a link (see /v1/projects/{projectId}/org-secrets).","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"responses":{"200":{"description":"The organization's secrets, masked","content":{"application/json":{"schema":{"type":"object","properties":{"secrets":{"type":"array","items":{"$ref":"#/components/schemas/Secret"}}}}}}},"400":{"description":"orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope (requires secrets:read), the viewer role, or the caller is not a member of the organization (`org_forbidden`), a machine credential named an organization other than its own (`org_assertion_mismatch`), or the organization a machine credential is bound to has been deleted (`org_deleted`)"},"500":{"description":"Internal server error"}},"tags":["Secrets"]},"post":{"summary":"Create or update an organization secret","description":"Creates an organization secret, or replaces the value of the one with the same name. The value is encrypted at rest and never returned. A name reserved by a platform-managed secret is refused.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","value"],"properties":{"name":{"type":"string","maxLength":255,"pattern":"^[a-zA-Z_][a-zA-Z0-9_]*$"},"value":{"type":"string","maxLength":10000}}}}}},"responses":{"200":{"description":"The existing secret's value was replaced","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Secret"}}}},"201":{"description":"The secret was created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Secret"}}}},"400":{"description":"Invalid org id (`invalid_org_id`), X-Org-Name does not name that org (`org_path_mismatch`), or the JSON body is not valid"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope (requires secrets:write), the viewer role, or the caller is not a member of the organization (`org_forbidden`), a machine credential named an organization other than its own (`org_assertion_mismatch`), or the organization a machine credential is bound to has been deleted (`org_deleted`)"},"409":{"description":"The name is reserved by a platform-managed secret (code: `secret_name_reserved`)"},"413":{"description":"Request body too large"},"500":{"description":"Internal server error"}},"tags":["Secrets"]}},"/v1/orgs/{orgId}/secrets/{secretName}":{"get":{"summary":"Get an organization secret","description":"Returns one organization secret's metadata. The value is always masked.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"secretName","required":true,"schema":{"type":"string"},"description":"The name of the secret"},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"responses":{"200":{"description":"The secret, masked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Secret"}}}},"400":{"description":"Invalid org id (`invalid_org_id`), X-Org-Name does not name that org (`org_path_mismatch`), or invalid secret name"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope (requires secrets:read), the viewer role, or the caller is not a member of the organization (`org_forbidden`), a machine credential named an organization other than its own (`org_assertion_mismatch`), or the organization a machine credential is bound to has been deleted (`org_deleted`)"},"404":{"description":"Secret not found"},"500":{"description":"Internal server error"}},"tags":["Secrets"]},"delete":{"summary":"Delete an organization secret","description":"Deletes an organization secret and every project link to it. Deployments that reference it stop receiving it on their next deploy.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"secretName","required":true,"schema":{"type":"string"},"description":"The name of the secret"},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"responses":{"204":{"description":"Secret deleted"},"400":{"description":"Invalid org id (`invalid_org_id`), X-Org-Name does not name that org (`org_path_mismatch`), or invalid secret name"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope (requires secrets:write), the caller is not an admin or owner, or not a member of the organization (`org_forbidden`), a machine credential named an organization other than its own (`org_assertion_mismatch`), or the organization a machine credential is bound to has been deleted (`org_deleted`)"},"404":{"description":"Secret not found"},"500":{"description":"Internal server error"}},"tags":["Secrets"]}},"/v1/members":{"get":{"summary":"List organization members","description":"Returns all members of the authenticated organization.","security":[{"BearerAuth":[]}],"responses":{"200":{"description":"List of organization members","content":{"application/json":{"schema":{"type":"object","properties":{"members":{"type":"array","items":{"type":"object","properties":{"user_id":{"type":"integer"},"username":{"type":"string"},"roles":{"type":"array","items":{"type":"string"}},"is_owner":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"}}}}}}}}},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"}},"tags":["Members"],"parameters":[{"$ref":"#/components/parameters/OrgName"}]},"post":{"summary":"Add a member","description":"Adds a new member to the organization with the specified roles.","security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["username"],"properties":{"username":{"type":"string","description":"The username of the user to add"},"roles":{"type":"array","items":{"type":"string"},"description":"Roles to assign to the new member","default":["member"]}}}}}},"responses":{"201":{"description":"Member added successfully","content":{"application/json":{"schema":{"type":"object","properties":{"user_id":{"type":"integer"},"username":{"type":"string"},"roles":{"type":"array","items":{"type":"string"}},"created_at":{"type":"string","format":"date-time"}}}}}},"400":{"description":"Invalid request body"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope, not an org admin/owner, or only owners can assign the admin or owner role"},"404":{"description":"User not found"},"409":{"description":"User is already a member of this organization"}},"tags":["Members"],"parameters":[{"$ref":"#/components/parameters/OrgName"}]}},"/v1/members/{memberId}":{"get":{"summary":"Get a member","description":"Returns details for a specific organization member, including their roles.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"memberId","required":true,"schema":{"type":"integer"},"description":"The user ID of the member"},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"Member details","content":{"application/json":{"schema":{"type":"object","properties":{"user_id":{"type":"integer"},"username":{"type":"string"},"roles":{"type":"array","items":{"type":"string"}},"is_owner":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"}}}}}},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"},"404":{"description":"Member not found"}},"tags":["Members"]},"patch":{"summary":"Update a member's roles","description":"Updates the roles assigned to a specific organization member. Organization owners cannot have their roles modified.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"memberId","required":true,"schema":{"type":"integer"},"description":"The user ID of the member"},{"$ref":"#/components/parameters/OrgName"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["roles"],"properties":{"roles":{"type":"array","items":{"type":"string"},"description":"New roles to assign to the member"}}}}}},"responses":{"200":{"description":"Member roles updated successfully","content":{"application/json":{"schema":{"type":"object","properties":{"user_id":{"type":"integer"},"username":{"type":"string"},"roles":{"type":"array","items":{"type":"string"}},"created_at":{"type":"string","format":"date-time"}}}}}},"400":{"description":"Invalid request body"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope, not an org admin/owner, cannot modify owner, only owners can modify/assign admin or owner roles"},"404":{"description":"Member not found, or the membership was removed before the role change was written"},"413":{"description":"Request body too large"},"500":{"description":"The organization could not be read, or the role change failed"}},"tags":["Members"]},"delete":{"summary":"Remove a member","description":"Removes a member from the organization. Organization owners cannot be removed.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"memberId","required":true,"schema":{"type":"integer"},"description":"The user ID of the member"},{"$ref":"#/components/parameters/OrgName"}],"responses":{"204":{"description":"Member removed successfully"},"400":{"description":"Invalid member ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope, not an org admin/owner, cannot remove owner, or only owners can remove admins or owners"},"404":{"description":"Member not found"},"500":{"description":"The removal could not be completed, or could not be confirmed"}},"tags":["Members"]}},"/v1/limits":{"get":{"summary":"Get organization limits","description":"Returns the current plan limits for the authenticated organization, including resource limits when private compute is enabled.","security":[{"BearerAuth":[]}],"responses":{"200":{"description":"Organization limits","content":{"application/json":{"schema":{"type":"object","properties":{"apps":{"type":"integer","description":"Maximum number of apps (-1 means unlimited)"},"replicas":{"type":"integer","description":"Maximum replicas per app (-1 means unlimited)"},"spicepods":{"type":"integer","description":"Maximum spicepods per app (-1 means unlimited)"},"users":{"type":"integer","description":"Maximum organization members (-1 means unlimited)"},"api_rps":{"type":"integer","description":"API requests per second limit (-1 means unlimited)"},"request_timeout_secs":{"type":"integer","description":"Request timeout in seconds"},"sql_query_timeout_secs":{"type":"integer","description":"SQL query timeout in seconds"},"sql_concurrent_queries":{"type":"integer","description":"Maximum concurrent SQL queries (-1 means unlimited)"},"resources":{"type":["object","null"],"description":"Resource limits for private compute; `null` when private compute is not enabled","properties":{"max_cpu":{"type":"integer","description":"Maximum vCPUs per container"},"max_memory_gi":{"type":"integer","description":"Maximum memory in Gi per container"}}}}}}}},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"},"404":{"description":"Organization not found"}},"tags":["Limits"],"parameters":[{"$ref":"#/components/parameters/OrgName"}]}},"/v1/regions":{"get":{"summary":"List regions","description":"Returns the available regions where apps can be deployed.","security":[{"BearerAuth":[]}],"responses":{"200":{"description":"List of available regions","content":{"application/json":{"schema":{"type":"object","properties":{"regions":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","description":"Human-readable region name"},"region":{"type":"string","description":"Region identifier (e.g., us-east-2)"},"provider":{"type":"string","enum":["teraswitch","aws","azure"]},"providerName":{"type":"string","enum":["Teraswitch","AWS","Azure"]},"isDefault":{"type":"boolean"},"disabled":{"type":"boolean"}}}},"default":{"type":"string","description":"The default region identifier"}}}}}},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"}},"tags":["Regions"],"parameters":[{"$ref":"#/components/parameters/OrgName"}]}},"/openapi.json":{"get":{"summary":"Get OpenAPI specification","description":"Returns the OpenAPI specification for the Spice Cloud Control-Plane API.","responses":{"200":{"description":"OpenAPI specification document","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Documentation"]}},"/v1/apps":{"get":{"summary":"List projects","description":"Legacy alias of `/v1/projects`. Projects were previously called apps. Returns projects belonging to the authenticated organization. Page with `limit` (default 1000, max 1000). Prefer `after_id` for a stable keyset (descending `id`). `offset` is accepted for compatibility. PostgREST also caps a page at 1000 rows.","security":[{"BearerAuth":[]}],"parameters":[{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":1000,"default":1000},"description":"Maximum number of projects to return"},{"in":"query","name":"offset","schema":{"type":"integer","minimum":0,"default":0},"description":"Number of projects to skip when a cursor is not supplied"},{"in":"query","name":"after_id","schema":{"type":"integer","minimum":1},"description":"Return projects with a strictly smaller id than this value."},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"List of projects","content":{"application/json":{"schema":{"type":"object","properties":{"apps":{"type":"array","items":{"$ref":"#/components/schemas/Project"}}}}}}},"400":{"description":"Invalid limit, offset, or after_id"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"}},"tags":["Projects"],"deprecated":true},"post":{"summary":"Create a project","description":"Legacy alias of `/v1/projects`. Projects were previously called apps. Creates a new project in the authenticated organization.","security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":4,"pattern":"^[a-zA-Z0-9-]+$","description":"Project name (letters, numbers, hyphens only)"},"region":{"type":"string","enum":["us-east-1","us-west-2"],"description":"Cloud region where a managed project will be deployed (e.g. `us-east-1`). Omit this, `cname`, and `cluster_name` to create an unattached standalone project whose placement is resolved when linked.\n"},"cname":{"type":"string","description":"**Deprecated.** Internal region CNAME (e.g. `us-east-1-prod-aws-data`). Use `region` instead. Accepted for backwards compatibility when `region` is omitted. Omit all region sources to create an unattached standalone project.\n"},"cluster_name":{"type":"string","description":"Name of the dedicated cluster to deploy to, as listed by `GET /v1/clusters`. Provide exactly one of `region` or `cluster_name`. When set, the region/routing are derived from the cluster; if `region` is also provided it must match the cluster's region. Omit all region sources to create an unattached standalone project.\n"},"description":{"type":"string","description":"Project description"},"visibility":{"type":"string","enum":["public","private"],"default":"private"},"default_alerts":{"type":"boolean","default":true,"description":"Create default monitoring alerts for the new managed project. False skips initial setup. Manual alert creation remains available."},"tags":{"type":"object","additionalProperties":{"type":"string"},"description":"Key-value tags for the project"},"update_channel":{"type":"string","enum":["stable","preview","nightly"],"description":"Update channel for a managed project deployment. A standalone project has no spicepod until linking, so configure its channel after linking."},"replicas":{"type":"integer","minimum":0,"description":"Number of replicas for a managed project. Rejected for standalone projects."},"resources":{"type":"object","description":"Resource requests and limits for a managed project container. Rejected for standalone projects.","properties":{"limits":{"type":"object","properties":{"cpu":{"type":"string","description":"Whole-number vCPU limit, or '-' for no CPU limit"},"memory":{"type":"string","description":"Memory limit in Gi (for example, 16Gi)"},"ephemeral-storage":{"type":"string","description":"Ephemeral storage limit in Gi (for example, 8Gi)"}}},"requests":{"type":"object","properties":{"cpu":{"type":"string","description":"CPU request. Minimum 100 millicores (for example, 100m or 0.1)."}}}}},"executor":{"type":"object","description":"Executor container configuration for a managed project. Rejected for standalone projects.","properties":{"replicas":{"type":"integer","minimum":0,"description":"Number of executor replicas"},"resources":{"type":"object","description":"Resource requests and limits for the executor container","properties":{"limits":{"type":"object","properties":{"cpu":{"type":"string","description":"Whole-number vCPU limit, or '-' for no CPU limit"},"memory":{"type":"string","description":"Memory limit in Gi (for example, 16Gi)"},"ephemeral-storage":{"type":"string","description":"Ephemeral storage limit in Gi (for example, 8Gi)"}}},"requests":{"type":"object","properties":{"cpu":{"type":"string","description":"CPU request. Minimum 100 millicores (for example, 100m or 0.1)."}}}}}}}}}}}},"responses":{"201":{"description":"Project created successfully. The response may include a `warning` field if initial configuration could not be applied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectWithConfig"}}}},"400":{"description":"Invalid request body or invalid region"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope, dedicated-cluster assignment or special-org private-compute ceilings required for resource limits, or requested resources exceed dedicated-cluster capacity"},"404":{"description":"Organization not found, including an organization deleted while the project was being created"},"409":{"description":"A project with this name already exists, or the project was deleted while its initial configuration was being written (`code` is `project_deleted`)"},"429":{"description":"Too many project creation requests (rate limited)"},"500":{"description":"Project creation failed, including when the name-collision check could not be read","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"Generic failure reason; the underlying cause stays in the server logs","example":"Failed to create project"}}}}}}},"tags":["Projects"],"parameters":[{"$ref":"#/components/parameters/OrgName"}],"deprecated":true}},"/v1/apps/{appId}":{"get":{"summary":"Get a project","description":"Legacy alias of `/v1/projects/{projectId}`. Projects were previously called apps. Returns details for a specific project, including its configuration.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"appId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"Project details with configuration","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"},"kind":{"type":"string","enum":["managed","standalone","byoc"],"description":"Resolved project hosting model"},"description":{"type":"string"},"visibility":{"type":"string","enum":["public","private"]},"created_at":{"type":"string","format":"date-time"},"cluster_name":{"type":["string","null"],"description":"Resolved dedicated cluster name; null for projects not assigned to a dedicated cluster."},"endpoint":{"type":["string","null"],"description":"The data-plane endpoint the project connects to, or null before a standalone instance is attached."},"production_branch":{"type":"string"},"api_key":{"type":"string","deprecated":true,"description":"Deprecated. Use the API Keys endpoints instead."},"tags":{"type":"object","additionalProperties":{"type":"string"}},"forked_from":{"type":["object","null"],"description":"The project this project was forked from, while that project is live in the same organization. Null for a project that was not created as a fork.\n","required":["id","name"],"properties":{"id":{"type":"integer","description":"ID of the source project"},"name":{"type":"string","description":"Name of the source project"}}},"paused_at":{"type":["string","null"],"format":"date-time","description":"Timestamp when the project was paused; null while running"},"config":{"type":"object","properties":{"spicepod":{"type":"object"},"image_tag":{"type":"string","description":"Runtime image tag"},"update_channel":{"type":["string","null"],"enum":["stable","preview","nightly",null],"description":"Update channel for the runtime, or null when Spice operates this project's runtime updates."},"version":{"type":"string","description":"Semver range for the runtime version (e.g. 1.x, 2.x)"},"replicas":{"type":"integer"},"resources":{"type":["object","null"],"description":"Resource requests and limits for the project container","properties":{"limits":{"type":"object","properties":{"cpu":{"type":"string","description":"Whole-number vCPU limit, or '-' for no CPU limit"},"memory":{"type":"string","description":"Memory limit in Gi (for example, 16Gi)"},"ephemeral-storage":{"type":"string"}}},"requests":{"type":"object","properties":{"cpu":{"type":"string","description":"CPU request. Minimum 100 millicores (for example, 100m or 0.1)."}}}}},"executor":{"type":["object","null"],"description":"Executor container configuration","properties":{"replicas":{"type":"integer","description":"Number of executor replicas"},"resources":{"type":"object","description":"Resource requests and limits for the executor container","properties":{"limits":{"type":"object","properties":{"cpu":{"type":"string","description":"Whole-number vCPU limit, or '-' for no CPU limit"},"memory":{"type":"string","description":"Memory limit in Gi (for example, 16Gi)"},"ephemeral-storage":{"type":"string"}}},"requests":{"type":"object","properties":{"cpu":{"type":"string","description":"CPU request. Minimum 100 millicores (for example, 100m or 0.1)."}}}}},"storage_size_gb":{"type":["number","null"]}}},"region":{"type":["string","null"],"description":"Server-resolved region; null before a standalone instance is attached."},"storage_size_gb":{"type":["number","null"]},"storage_claim_size_gb":{"type":"number","deprecated":true,"description":"Deprecated. Use storage_size_gb instead."}}}}}}}},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"},"404":{"description":"Project not found"},"500":{"description":"Internal server error"}},"tags":["Projects"],"deprecated":true},"put":{"summary":"Update a project","description":"Legacy alias of `/v1/projects/{projectId}`. Projects were previously called apps. Updates a project's metadata and configuration, including description, visibility, spicepod configuration, replicas, and resource limits.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"appId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"$ref":"#/components/parameters/OrgName"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"description":{"type":"string"},"visibility":{"type":"string","enum":["public","private"]},"production_branch":{"type":"string"},"tags":{"type":"object","additionalProperties":{"type":"string"},"description":"Key-value tags for the project. The map replaces the stored tags. System tags the project already has stay on the project when the map omits them.\n"},"spicepod":{"oneOf":[{"type":"string","description":"YAML string of the spicepod configuration"},{"type":"object","description":"JSON object of the spicepod configuration"}],"description":"The project's Spicepod. A project connected to a GitHub repository takes its Spicepod from the repository's spicepod.yaml, so for such a project only the stored Spicepod, sent back unchanged, is accepted."},"image_tag":{"type":["string","null"],"description":"Runtime image tag for a managed project; rejected for standalone projects. A non-empty value pins the project. On stable, the pin must be a published stable runtime version. On preview, it must be a published preview or stable runtime version. On stable or preview, send null to clear a pin so deploys resolve from the channel catalog (stable - latest GA for the configured version range; preview - latest RC). null is rejected on nightly and internal channels."},"update_channel":{"type":"string","enum":["stable","preview","nightly"],"description":"Update channel for a managed runtime; rejected for standalone projects."},"version":{"type":"string","description":"Semver range for a managed runtime version (e.g. 1.x, 2.x); rejected for standalone projects."},"replicas":{"type":"integer","minimum":0,"description":"Managed runtime replica count; rejected for standalone projects."},"resources":{"type":"object","description":"Resource requests and limits for a managed project container; rejected for standalone projects.","properties":{"limits":{"type":"object","properties":{"cpu":{"type":"string","description":"Whole-number vCPU limit, or '-' for no CPU limit"},"memory":{"type":"string","description":"Memory limit in Gi (for example, 16Gi)"},"ephemeral-storage":{"type":"string","description":"Ephemeral storage limit in Gi (for example, 8Gi)"}}},"requests":{"type":"object","properties":{"cpu":{"type":"string","description":"CPU request. Minimum 100 millicores (for example, 100m or 0.1)."}}}}},"executor":{"type":"object","description":"Executor container configuration for a managed project; rejected for standalone projects.","properties":{"replicas":{"type":"integer","minimum":0,"description":"Number of executor replicas"},"resources":{"type":"object","description":"Resource requests and limits for the executor container","properties":{"limits":{"type":"object","properties":{"cpu":{"type":"string","description":"Whole-number vCPU limit, or '-' for no CPU limit"},"memory":{"type":"string","description":"Memory limit in Gi (for example, 16Gi)"},"ephemeral-storage":{"type":"string","description":"Ephemeral storage limit in Gi (for example, 8Gi)"}}},"requests":{"type":"object","properties":{"cpu":{"type":"string","description":"CPU request. Minimum 100 millicores (for example, 100m or 0.1)."}}}}},"storage_size_gb":{"type":"number"}}},"region":{"type":"string","description":"Managed project region; standalone placement is resolved during attachment."},"cluster_name":{"type":"string","description":"Reassign the project to a dedicated cluster, as listed by `GET /v1/clusters`. Provide at most one of `region` or `cluster_name`. When set, region/routing are derived from the cluster; if `region` is also provided it must match the cluster's region. Rejected for standalone projects, whose placement is resolved during attachment.\n"},"storage_size_gb":{"type":"number","description":"Managed runtime storage size; rejected for standalone projects."},"storage_claim_size_gb":{"type":"number","deprecated":true,"description":"Deprecated. Use storage_size_gb instead. Rejected for standalone projects."}}}}}},"responses":{"200":{"description":"Project updated successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectWithConfig"}}}},"400":{"description":"Invalid request body or project ID, including placement or hosted runtime fields that do not apply to standalone projects. When `image_tag` is supplied on stable or preview, a tag that is not a published version for that channel returns `{ error: 'Invalid image_tag for stable channel', code: 'invalid_stable_image_tag' }` (or the preview analog). A `spicepod` that adds a component on a source the Spice runtime no longer loads returns code `retired_component_source`; components the project already stores on such a source may be kept, edited and renamed, and a project backed by a GitHub repository is not checked. A `spicepod` that adds a plain-text secret (a password, token, API key, private key, or URL password in a component param) returns code `plaintext_secrets` with `plaintext_secrets` listing each path. Store the value as a project or org secret and use `${secrets:NAME}` in its place. Secrets the stored spicepod already holds do not block the update."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope, viewer role cannot mutate projects, requested replicas or resources exceed plan limits, dedicated-cluster assignment or special-org private-compute ceilings required, requested resources exceed dedicated-cluster capacity, `image_tag` was supplied outside the Enterprise plan (code: `image_tag_requires_enterprise`), or the requested runtime image is not available for the organization plan (code: `image_tag_not_allowed_for_plan`); omitting `image_tag` leaves the configured image unchanged"},"404":{"description":"Project not found"},"409":{"description":"The project's lifecycle state changed while the update was in flight (a pause, resume, or restore landed at the same time), so the configuration was not saved (code: `project_state_changed`). Re-read the project before retrying: a configuration update clears `paused_at`, so repeating this request against a project that has since been paused also resumes it. Or a `spicepod` that differs from the stored one was sent for a project connected to a GitHub repository (code: `github_connected`); change spicepod.yaml in the repository instead, since nothing in that request is applied."},"500":{"description":"Internal server error"},"502":{"description":"The configuration was stored but could not be published to the spicepod registry (code: `registry_publish_failed`), so the project still deploys its previously published configuration. Retry the request."},"503":{"description":"Runtime version catalog is unavailable"}},"tags":["Projects"],"deprecated":true},"delete":{"summary":"Delete a project","description":"Legacy alias of `/v1/projects/{projectId}`. Projects were previously called apps. Deletes a project and tears down its runtime resources.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"appId","required":true,"schema":{"type":"integer"},"description":"The ID of the project to delete"},{"$ref":"#/components/parameters/OrgName"}],"responses":{"204":{"description":"Project deleted successfully"},"400":{"description":"Invalid project ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope or viewer role cannot mutate projects"},"404":{"description":"Project not found"},"500":{"description":"Internal server error"}},"tags":["Projects"],"deprecated":true}},"/v1/apps/{appId}/forks":{"get":{"summary":"List forks of a project","description":"Legacy alias of `/v1/projects/{projectId}/forks`. Projects were previously called apps. Returns the live projects in the organization that were forked from this project, ordered by name. Requires `apps:read`.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"appId","required":true,"schema":{"type":"integer"},"description":"The ID of the source project"},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"Forks of the project","content":{"application/json":{"schema":{"type":"object","required":["forks"],"properties":{"forks":{"type":"array","items":{"$ref":"#/components/schemas/Project"}}}}}}},"400":{"description":"Invalid project ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"},"404":{"description":"Project not found"},"500":{"description":"Internal server error"}},"tags":["Projects"],"deprecated":true},"post":{"summary":"Fork a project","description":"Legacy alias of `/v1/projects/{projectId}/forks`. Projects were previously called apps. Creates a new project from this project's configuration and records this project as its source. The fork copies the spicepod the project deploys, its linked connections, project secrets, linked organization secrets, update channel, version range, replica and executor counts, storage size, description, and tags. It does not copy API keys (new ones are generated), deployments, the GitHub repository link, the instance size, or visibility (forks are private). The fork is not deployed: deploy it with POST /v1/projects/{projectId}/deployments. Requires `apps:write`. The server copies secret values without returning them, so no secrets scope is needed. A source connected to a GitHub repository is read from that repository, and its spicepod is copied only when it loads no other file from the repository; to deploy a fork from a fork of the repository, fork the project in the Spice portal.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"appId","required":true,"schema":{"type":"integer"},"description":"The ID of the project to fork"},{"$ref":"#/components/parameters/OrgName"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"name":{"type":"string","minLength":4,"maxLength":38,"pattern":"^[a-zA-Z0-9-]+$","description":"Name of the fork. Defaults to the source name with `-fork`, then `-fork-2` and so on, whichever is free.\n"},"region":{"type":"string","enum":["us-east-1","us-west-2"],"description":"Region to place the fork in. Omit it and `cluster_name` to place the fork where the source runs.\n"},"cluster_name":{"type":"string","description":"Dedicated cluster to place the fork on, as listed by `GET /v1/clusters`. If `region` is also set, it must match the cluster's region.\n"},"scheduler_state_location":{"type":"string","maxLength":2048,"description":"s3:// URI for the fork's scheduler state. Required when the source spicepod sets `runtime.scheduler.state_location`, and must not overlap it: two schedulers cannot share a location.\n"},"default_alerts":{"type":"boolean","default":true,"description":"Create default monitoring alerts for the new managed fork. False skips initial setup. Manual alert creation remains available."}}}}}},"responses":{"201":{"description":"Project forked. It has not been deployed yet.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ProjectWithConfig"},{"type":"object","required":["shared_state"],"properties":{"shared_state":{"type":"array","description":"Settings copied as they are that name external state the source also uses: a replication slot or Kafka consumer group the spicepod names, or a snapshot location. Change them in the fork's spicepod before deploying it if the two projects should not share that state.\n","items":{"type":"object","required":["component","setting","value"],"properties":{"component":{"type":"string","description":"The dataset, or `snapshots`"},"setting":{"type":"string","example":"pg_replication_slot"},"value":{"type":"string"}}}}}}]}}}},"400":{"description":"Invalid project ID or request body, an unknown region or cluster, no `region` or `cluster_name` set for a source whose placement a fork cannot reuse (a BYOC cluster, a self-hosted runtime, or a region or cluster that is no longer available; `code: fork_placement_required`), or a missing or unusable `scheduler_state_location` (`code: scheduler_state_location_required`, `scheduler_state_location_unsupported`, `scheduler_state_location_overlaps_source`, or `scheduler_state_location_not_applicable`).\n"},"401":{"description":"Unauthorized"},"402":{"description":"The source runs models on hosted AI credits, which the organization has used up (`code: ai_credits_exhausted`).\n"},"403":{"description":"Insufficient scope, a viewer role, or the source runs more replicas or executors than the plan or the dedicated cluster allows\n"},"404":{"description":"Project or organization not found"},"409":{"description":"A project with the requested name already exists, another project creation is in progress, or every default name is taken (`code: fork_name_unavailable`)\n"},"413":{"description":"Request body too large"},"422":{"description":"The source has no spicepod (`code: fork_source_has_no_spicepod`), its spicepod is not valid (`code: fork_source_spicepod_invalid`), or its spicepod loads files from its GitHub repository besides spicepod.yaml (`code: fork_source_loads_repository_files`)\n"},"429":{"description":"Too many project creation requests (rate limited)"},"500":{"description":"The fork could not be created. When the failure comes after the project row is created, the fork is deleted again with its spicepod, secrets, and organization secret links. If the project itself cannot be deleted, the response has `code: fork_rollback_failed` and names the partially created project in `project` (`id` and `name`). Delete that project, by `project.id`, before reusing its name.\n"},"502":{"description":"The source is connected to a GitHub repository, and its spicepod could not be read from it (`code: fork_source_repository_unreadable`)\n"}},"tags":["Projects"],"deprecated":true}},"/v1/apps/{appId}/pause":{"post":{"summary":"Pause a project","description":"Legacy alias of `/v1/projects/{projectId}/pause`. Projects were previously called apps. Pauses the project's running spicepod. The runtime is torn down and the\nspicepod record is marked as paused. Settings and configuration are\npreserved. Resume with POST /v1/projects/{projectId}/resume. Requires\n`apps:write`.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"appId","required":true,"schema":{"type":"integer"},"description":"The ID of the project to pause"},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"Project paused","content":{"application/json":{"schema":{"type":"object","required":["id","name","paused_at"],"properties":{"id":{"type":"integer","description":"The project ID"},"name":{"type":"string","description":"The project name"},"paused_at":{"type":"string","format":"date-time","description":"Timestamp when the project was paused"}}}}}},"400":{"description":"Invalid project ID, missing API key, no spicepod configured, or spicepod has been deleted"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"},"404":{"description":"Project not found"},"409":{"description":"Project is already paused","content":{"application/json":{"schema":{"type":"object","required":["error","paused_at"],"properties":{"error":{"type":"string"},"paused_at":{"type":"string","format":"date-time","description":"Timestamp of the existing pause"}}}}}},"500":{"description":"Internal server error"}},"tags":["Projects"],"deprecated":true}},"/v1/apps/{appId}/resume":{"post":{"summary":"Resume a paused project","description":"Legacy alias of `/v1/projects/{projectId}/resume`. Projects were previously called apps. Clears the paused state on the project's spicepod and creates a new\ndeployment to bring the runtime back up. A project connected to a\nGitHub repository deploys spicepod.yaml from its root directory on its\nproduction branch, read when the project resumes. Requires\n`apps:write`.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"appId","required":true,"schema":{"type":"integer"},"description":"The ID of the project to resume"},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"Project resumed, deployment created","content":{"application/json":{"schema":{"type":"object","required":["id","name","paused_at","deployment_id"],"properties":{"id":{"type":"integer","description":"The project ID"},"name":{"type":"string","description":"The project name"},"paused_at":{"type":["string","null"],"format":"date-time","description":"Always null after a successful resume"},"deployment_id":{"type":"integer","description":"ID of the deployment created to bring the runtime back up"}}}}}},"400":{"description":"Invalid project ID, missing API key, missing project organization, no spicepod configured, spicepod has been deleted, or the spicepod `name`, `kind` or `version` is missing or not valid (the body carries an `issues` array of `{path, message}`). For a project connected to a GitHub repository, also returned when the root directory has no spicepod.yaml or it is invalid or too large (`code: spicepod_not_found`, `spicepod_invalid` or `spicepod_too_large`). The project stays paused.\n"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope, or the org's GitHub App installation cannot read the connected repository (`code: github_unauthorized` or `github_forbidden`). Also returned when the installation was removed (`code: github_installation_removed`) or suspended (`code: github_installation_suspended`), or when it cannot read the connected repository because the repository is outside its repository access (`code: github_repository_unreachable`). The project stays paused.\n"},"404":{"description":"Project not found, or the connected GitHub repository or its production branch was not found (`code: github_not_found`). The project stays paused.\n"},"409":{"description":"Project is not paused, or its spicepod was deleted after the request read it. Also returned when another deployment of the project is queued (`code: deployment_in_progress`); the project then stays paused. Also returned when the project's repository, root directory, or production branch changes while the spicepod is read from the repository; the project then stays paused. Resume again.\n"},"422":{"description":"No runtime version in the catalog matches this project’s version range (`code: version_no_match`), or the spicepod uses version v1beta1, which the Spice 2 runtime cannot load (`code: spicepod_version_unsupported`). Also returned when the spicepod uses a connection credential that holds a secret reference deploy cannot resolve (`code: connection_credentials_unresolved`, with an `unresolved_credentials` array of `{connection, field}`). The project stays paused.\n"},"429":{"description":"GitHub rate-limited the read of the connected repository (`code: github_rate_limited`). The project stays paused.\n"},"500":{"description":"Internal server error"},"502":{"description":"GitHub did not answer while the connected repository was read (`code: github_unavailable`). The project stays paused.\n"},"503":{"description":"The runtime version catalog is unavailable or empty (`code: version_source_unavailable`), so an unpinned stable or preview resume cannot resolve an image tag.\n"}},"tags":["Projects"],"deprecated":true}},"/v1/apps/{appId}/repository":{"put":{"summary":"Connect a project to a GitHub repository","description":"Legacy alias of `/v1/projects/{projectId}/repository`. Projects were previously called apps. Connects the project to a repository in the organization's GitHub\naccount, through the organization's Spice Cloud GitHub App\ninstallation. The next deployment reads spicepod.yaml (or\nspicepod.yml) from `root_directory` on `production_branch`.\nConnecting sets the project description to the repository\ndescription. Connecting again replaces the repository, root directory\nand production branch.\nRequires `apps:write`.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"appId","required":true,"schema":{"type":"integer"},"description":"The ID of the project to connect"},{"$ref":"#/components/parameters/OrgName"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["repository"],"properties":{"repository":{"type":"string","description":"Repository in the organization's GitHub account, as `name` or `owner/name`. The owner must be the organization's GitHub account.","example":"cookbook"},"root_directory":{"type":["string","null"],"maxLength":512,"description":"Directory inside the repository that contains spicepod.yaml. Omit it, or send null or an empty string, for the repository root.","example":"cayenne"},"production_branch":{"type":"string","maxLength":255,"description":"Branch that deployments read. Defaults to the repository's default branch. A branch that GitHub renamed is stored under its new name.","example":"trunk"}}}}}},"responses":{"200":{"description":"Project connected","content":{"application/json":{"schema":{"type":"object","required":["project_id","repository","repository_id","root_directory","production_branch","connected_at","has_spicepod"],"properties":{"project_id":{"type":"integer","description":"The project ID"},"repository":{"type":"string","description":"The repository as `owner/name`"},"repository_id":{"type":"integer","description":"The GitHub repository ID"},"root_directory":{"type":["string","null"],"description":"The stored root directory. Null is the repository root."},"production_branch":{"type":"string","description":"The branch that deployments read"},"connected_at":{"type":"string","format":"date-time","description":"When the project was connected"},"has_spicepod":{"type":"boolean","description":"True when spicepod.yaml or spicepod.yml exists in the root directory on the production branch. A deployment fails until it does."}}}}}},"400":{"description":"Invalid project ID or request body, a repository outside the organization's GitHub account (code: `invalid_repository`), or a root directory outside the repository (code: `invalid_root_directory`)"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope, or viewer role cannot mutate projects"},"404":{"description":"Project not found, or the organization's GitHub App installation cannot read the repository (code: `repository_not_found`)"},"409":{"description":"The organization has no usable GitHub App installation (code: `github_not_connected`), or another project already deploys this repository and root directory (code: `root_directory_conflict`)"},"413":{"description":"Request body too large"},"422":{"description":"The repository has no branch named `production_branch` (code: `branch_not_found`), the project has no spicepod configuration (code: `spicepod_missing`), or the organization is not fully configured"},"500":{"description":"Internal server error"},"503":{"description":"GitHub did not answer (code: `github_unavailable`)"}},"tags":["Projects"],"deprecated":true}},"/v1/apps/{appId}/metrics":{"get":{"summary":"Get project metrics","description":"Legacy alias of `/v1/projects/{projectId}/metrics`. Projects were previously called apps. Returns current resource utilization metrics (CPU, memory, disk I/O) and data ingestion metrics for a project.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"appId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"in":"query","name":"window","required":false,"schema":{"type":"string"},"description":"Prometheus-style duration (e.g. `5m`, `1h`, `30s`). When provided, counter metrics are returned as rates over the window instead of raw cumulative values. For example, `rows_ingested` returns average rows per second with a window, or total cumulative rows without one."},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"Resource and ingestion metrics. Missing fields indicate no data is currently available. Individual metric queries may fail independently; partial results are returned when possible.","content":{"application/json":{"schema":{"type":"object","properties":{"ingestion":{"type":"object","description":"Aggregated data ingestion metrics","properties":{"rows_ingested":{"type":"number","description":"Total rows ingested (without window) or rows per second (with window)"},"bytes_ingested":{"type":"number","description":"Total bytes ingested (without window) or bytes per second (with window)"}}},"cluster":{"type":"object","description":"Cluster metrics for the project","properties":{"active_executors_count":{"type":"number","description":"Number of active executors registered with the scheduler"}}},"metrics":{"type":"object","description":"Resource metrics keyed by pod name","additionalProperties":{"type":"object","properties":{"cpu_usage_percent":{"type":"number","description":"CPU usage as a percentage (0-100)"},"memory_usage_bytes":{"type":"number","description":"Memory usage in bytes"},"disk_read_bytes":{"type":"number","description":"Disk bytes read per second (with window) or cumulative (without window)"},"disk_read_operations":{"type":"number","description":"Disk read operations per second (with window) or cumulative count (without window)"},"disk_write_bytes":{"type":"number","description":"Disk bytes written per second (with window) or cumulative (without window)"},"disk_write_operations":{"type":"number","description":"Disk write operations per second (with window) or cumulative count (without window)"}}}}}}}}},"400":{"description":"Invalid project ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"},"404":{"description":"Project or organization not found"},"422":{"description":"Organization is not fully configured"},"500":{"description":"Internal server error"},"503":{"description":"Metrics service unavailable"}},"tags":["Projects"],"deprecated":true}},"/v1/apps/{appId}/instances":{"get":{"summary":"List project instances","description":"Legacy alias of `/v1/projects/{projectId}/instances`. Projects were previously called apps. Lists the runtime instances of a deployed project, newest first. A managed or BYOC project lists its Spicepod instances. A standalone project lists its Cloud Connect runtime. Read the logs of an instance with `GET /v1/projects/{projectId}/instances/{instanceName}/logs`.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"appId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"The project's instances","content":{"application/json":{"schema":{"type":"object","required":["instances"],"properties":{"instances":{"type":"array","items":{"$ref":"#/components/schemas/ProjectInstance"}}}}}}},"400":{"description":"Invalid project ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"},"404":{"description":"Project not found"},"409":{"description":"The project has no running instances. `code` is `project_not_deployed` when a managed or BYOC project was never deployed, or `project_paused` when the project is paused.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectRuntimeError"}}}},"422":{"description":"The project is not fully configured"},"500":{"description":"Internal server error"},"503":{"description":"The project runtime did not answer"}},"tags":["Projects"],"deprecated":true}},"/v1/apps/{appId}/instances/{instanceName}/logs":{"get":{"summary":"Get project instance logs","description":"Legacy alias of `/v1/projects/{projectId}/instances/{instanceName}/logs`. Projects were previously called apps. Returns the most recent log lines of one instance of a deployed project, oldest first, without color codes. The org's plan sets the largest `tail`. List the instance names with `GET /v1/projects/{projectId}/instances`.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"appId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"in":"path","name":"instanceName","required":true,"schema":{"type":"string"},"description":"The instance name from `GET /v1/projects/{projectId}/instances`"},{"in":"query","name":"tail","required":false,"schema":{"type":"integer","minimum":1,"default":100},"description":"How many of the most recent lines to return"},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"The instance's most recent log lines","content":{"application/json":{"schema":{"type":"object","required":["instance","tail","lines"],"properties":{"instance":{"type":"string","description":"The instance name"},"tail":{"type":"integer","description":"The number of lines asked for"},"lines":{"type":"array","items":{"type":"string"},"description":"Log lines, oldest first"}}}}}},"400":{"description":"Invalid project ID or `tail`"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope (`code` is `insufficient_scope`), or `tail` is larger than the org's plan allows (`code` is `plan_limit`, and `max_tail` is the largest allowed `tail`).","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"properties":{"error":{"type":"string"},"code":{"type":"string"},"max_tail":{"type":"integer"}}}}}},"404":{"description":"The project was not found, or the instance is not one of the project's instances (`code` is `instance_not_found`)."},"409":{"description":"The logs cannot be read in the project's current state. `code` is `project_not_deployed` when the project was never deployed, `project_paused` when it is paused, or `instance_offline` when the Cloud Connect runtime is offline and does not answer.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"properties":{"error":{"type":"string"},"code":{"type":"string","enum":["project_not_deployed","project_paused","instance_offline"]}}}}}},"422":{"description":"The project is not fully configured"},"500":{"description":"Internal server error"},"503":{"description":"The project runtime did not answer"}},"tags":["Projects"],"deprecated":true}},"/v1/apps/{appId}/deployments":{"get":{"summary":"List deployments","description":"Legacy alias of `/v1/projects/{projectId}/deployments`. Projects were previously called apps. Returns a list of deployments for the specified project, ordered by most recent first.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"appId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"in":"query","name":"limit","schema":{"type":"integer","default":20},"description":"Maximum number of deployments to return"},{"in":"query","name":"status","schema":{"type":"string","enum":["queued","in_progress","succeeded","failed","created"]},"description":"Filter by deployment status"},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"List of deployments","content":{"application/json":{"schema":{"type":"object","properties":{"deployments":{"type":"array","items":{"$ref":"#/components/schemas/Deployment"}}}}}}},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"},"404":{"description":"Project not found"}},"tags":["Deployments"],"deprecated":true},"post":{"summary":"Create a deployment","description":"Legacy alias of `/v1/projects/{projectId}/deployments`. Projects were previously called apps. Creates a new deployment for the specified project using its current spicepod configuration. A project connected to a GitHub repository deploys spicepod.yaml from its root directory, read when the deployment is created. It reads the head of the production branch, or the `branch` or `commit_sha` the request names. The runtime image is selected based on the project's update channel.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"appId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"$ref":"#/components/parameters/OrgName"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"image_tag":{"type":"string","description":"Override the runtime image tag for this deployment. Requires the Enterprise plan; omit it to deploy the image the project is configured with. When the effective update channel is `stable` or `preview`, a supplied tag must be a published version for that channel. When the effective update channel is `nightly`, a supplied tag must be a published `spicehq/spiceai` `nightly-*` tag (`YYYYMMDDHHmm` plus `-` plus 7 to 40 lowercase hex characters, for example `202609081124-cadf292`)."},"channel":{"type":"string","enum":["stable","preview","nightly"],"description":"Update channel that determines the runtime image"},"expected_update_channel":{"type":"string","enum":["stable","preview","nightly"],"description":"If set, the stored project update channel must match this value or the request is refused with 409 `update_channel_mismatch`. This is a precondition, not an override (`channel` still changes the image train).\n"},"replicas":{"type":"integer","minimum":1,"maximum":10,"description":"Override the number of replicas for this deployment"},"branch":{"type":"string","minLength":1,"maxLength":255,"description":"For a project connected to a GitHub repository, deploy spicepod.yaml from the head of this branch or tag of that repository instead of the production branch. The branch or tag must exist. With `commit_sha`, it must contain the commit. The production branch and the stored spicepod do not change, so the next deployment without a revision, and any automatic redeploy, deploy the production branch head again. A project with no connected repository refuses this field with 400 `repository_not_connected`. A project whose org has no GitHub App installation refuses it with 422 `github_installation_missing`. A request from the deploy GitHub Action (User-Agent `spice-cloud-deploy-action`) does not check or use this field for any project, because the Action sends the branch of its workflow run as metadata.\n"},"commit_sha":{"type":"string","pattern":"^[0-9a-fA-F]{7,40}$","description":"For a project connected to a GitHub repository, deploy spicepod.yaml from this commit instead of the head of the production branch. The commit must be on `branch`, or on the production branch (never a tag of the same name) when `branch` is omitted, so a commit that only a fork holds is refused. The deployment records the full SHA. The production branch and the stored spicepod do not change. A project with no connected repository refuses this field with 400 `repository_not_connected`. A project whose org has no GitHub App installation refuses it with 422 `github_installation_missing`. A request from the deploy GitHub Action (User-Agent `spice-cloud-deploy-action`) does not check or use this field for any project, because the Action sends the commit of its workflow run as metadata.\n"},"commit_message":{"type":"string","description":"Not used. A deployment from a connected repository records the message of the commit it reads.\n"},"debug":{"type":"boolean","description":"Enable debug mode for this deployment"},"target":{"type":"object","description":"Optional per-deployment override. Omit (or `{ kind: cloud }`) to place the deployment where the project itself runs: Spice-managed compute, its own BYOC cluster, or its attached standalone Cloud Connect instance. That placement is a property of the project, not of this request. Pass `{ kind: cluster, instance_id }` to override that for this deployment and route it to a BYOC cluster connection; the cluster must be owned by the token's org, not in a terminal lifecycle state (decommissioning/archived), not paused, and the operator must hold a live connection (verified by the connectivity ping, not the Supabase lifecycle status). A standalone project cannot be overridden this way and is refused with 409.\n","properties":{"kind":{"type":"string","enum":["cloud","cluster"]},"instance_id":{"type":"string","description":"Required when kind is `cluster`."}}}}}}}},"responses":{"202":{"description":"Deployment created and queued for processing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Deployment"}}}},"400":{"description":"Invalid request body, project has no spicepod configuration, the spicepod has been deleted, or the spicepod is paused (`code: spicepod_paused`; resume it with POST /v1/projects/{projectId}/resume before deploying). Also returned when the spicepod `name`, `kind` or `version` is missing or not valid: the body is `{ error: 'Invalid spicepod configuration' }` with an `issues` array of `{path, message}`. The runtime checks all other spicepod fields when it loads the spicepod. For a standalone Cloud Connect project, also returned when the Spicepod references a `${secrets:...}` name the project does not define: the body carries `code: unknown_secret_references`, a `missing_secrets` array of `{name, path, suggestion?}`, and `secrets_url`. When the effective update channel is `stable` or `preview` and `image_tag` is supplied, the tag must be a published version for that channel or the body is `{ error: 'Invalid image_tag for stable channel', code: 'invalid_stable_image_tag' }` (or the preview analog). When the effective update channel is `nightly` and `image_tag` is supplied, the tag must be a published `spicehq/spiceai` `nightly-*` tag of the form `YYYYMMDDHHmm` plus `-` plus 7 to 40 lowercase hex characters (for example `202609081124-cadf292`) or the body is `{ error: 'Invalid image_tag for nightly channel', code: 'invalid_nightly_image_tag' }`. For a project connected to a GitHub repository, also returned when the root directory has no spicepod.yaml or it is invalid or too large (`code: spicepod_not_found`, `spicepod_invalid` or `spicepod_too_large`), when `commit_sha` is not on `branch` (or on the production branch when `branch` is omitted) (`code: commit_not_on_branch`), or when `commit_sha` comes without `branch` and the project has no production branch (`code: branch_required`). Nothing is dispatched and no deployment is recorded. Also returned when `branch` or `commit_sha` is sent for a project with no connected repository (`code: repository_not_connected`), except from the deploy GitHub Action. Nothing is dispatched.\n"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope, viewer role cannot create deployments, `image_tag` was supplied outside the Enterprise plan (code: `image_tag_requires_enterprise`), the requested runtime image is not available for the organization plan (code: `image_tag_not_allowed_for_plan`), or the organization's GitHub App installation cannot read the connected repository (code: `github_unauthorized` or `github_forbidden`), was removed (code: `github_installation_removed`), is suspended (code: `github_installation_suspended`), or has no access to the connected repository (code: `github_repository_unreachable`); omitting `image_tag` deploys the image the project is configured with"},"404":{"description":"Project not found. For a `cluster` target, also returned when the BYOC cluster connection does not exist or is not owned by the token's org. Also returned when the connected GitHub repository, its production branch, or the requested `branch` or `commit_sha` was not found (`code: github_not_found`). With `commit_sha` and no `branch`, this includes a production branch that no longer exists.\n"},"409":{"description":"A deployment is already in progress for this project (`code: deployment_in_progress`, with optional `activeDeploymentId`). For a `cluster` target, also returned when the BYOC cluster is paused, offline, or otherwise not in a deployable state. Also returned when the project's deploy target is unusable: its kind conflicts with its cluster/instance references, a `cluster` target was requested for a standalone project, a standalone project has no attached instance, or that instance has not yet enrolled the encryption key its secrets are sealed to (retrying once it is online succeeds). Also returned when `expected_update_channel` does not match the stored spicepod update channel, or when that channel changes before dispatch (`code: update_channel_mismatch`). Also returned when the project is paused after the request checked it (`code: spicepod_paused`), or its spicepod is deleted after that check (`code: spicepod_deleted`). Also returned when the project's repository, root directory, or production branch changes while the spicepod is read from the repository; deploy again. Nothing is dispatched and no deployment is recorded.\n"},"413":{"description":"Request body exceeds the maximum size (1 MiB), or, for a standalone Cloud Connect project, the project's resolved secrets exceed the maximum sealed-payload size. Nothing is dispatched and no deployment is recorded.\n"},"422":{"description":"Organization is not fully configured, no runtime version in the catalog matches this project’s version range (`code: version_no_match`), or the spicepod uses version v1beta1, which the Spice 2 runtime cannot load (`code: spicepod_version_unsupported`). Also returned when the Spicepod uses a connection credential that holds a secret reference deploy cannot resolve: the body carries `code: connection_credentials_unresolved` and an `unresolved_credentials` array of `{connection, field}`. Nothing is deployed. Also returned when `branch` or `commit_sha` is sent for a project with a connected repository whose org has no GitHub App installation to read it (`code: github_installation_missing`), except from the deploy GitHub Action. Nothing is dispatched.\n"},"429":{"description":"GitHub rate-limited the read of the connected repository (`code: github_rate_limited`).\n"},"500":{"description":"Internal server error. For a standalone Cloud Connect project, also returned with `code: deployment_not_settled` when the deployment record could not be moved out of `queued`; the project's next deployment is refused until it is cleared.\n"},"502":{"description":"Upstream service failure while provisioning the deployment, or, for a standalone Cloud Connect project, the spicepod could not be handed to the instance's gateway, or the instance rejected it. Also returned when GitHub did not answer while the connected repository was read (`code: github_unavailable`).\n"},"503":{"description":"The runtime version catalog is unavailable or empty (`code: version_source_unavailable`), so an unpinned stable or preview deploy cannot resolve an image tag, or catalog membership for an explicit `image_tag` override cannot be validated. Also returned when the published nightly tag list cannot be loaded to validate a nightly `image_tag` override.\n"}},"tags":["Deployments"],"deprecated":true}},"/v1/apps/{appId}/deployments/{deploymentId}":{"get":{"summary":"Get a deployment","description":"Legacy alias of `/v1/projects/{projectId}/deployments/{deploymentId}`. Projects were previously called apps. Returns a single deployment for the specified project. The body matches one item from GET /v1/projects/{projectId}/deployments. Not cacheable. Poll this path for stored status after POST create.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"appId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"in":"path","name":"deploymentId","required":true,"schema":{"type":"integer"},"description":"The ID of the deployment"},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"Deployment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Deployment"}}}},"400":{"description":"Invalid project ID or deployment ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"},"404":{"description":"Project or deployment not found"},"500":{"description":"Internal server error"}},"tags":["Deployments"],"deprecated":true}},"/v1/apps/{appId}/secrets":{"get":{"summary":"List secrets","description":"Legacy alias of `/v1/projects/{projectId}/secrets`. Projects were previously called apps. Returns all secrets for the specified project. Secret values are always masked.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"appId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"List of secrets","content":{"application/json":{"schema":{"type":"object","properties":{"secrets":{"type":"array","items":{"$ref":"#/components/schemas/Secret"}}}}}}},"400":{"description":"Invalid project ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope or viewer role cannot perform write operations"},"404":{"description":"Project not found"},"500":{"description":"Internal server error"}},"tags":["Secrets"],"deprecated":true},"post":{"summary":"Create or update a secret","description":"Legacy alias of `/v1/projects/{projectId}/secrets`. Projects were previously called apps. Creates a new secret or updates an existing secret with the same name. The secret value is encrypted at rest.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"appId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"$ref":"#/components/parameters/OrgName"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","value"],"properties":{"name":{"type":"string","pattern":"^[a-zA-Z_][a-zA-Z0-9_]*$","description":"Secret name (must start with a letter or underscore, alphanumeric and underscores only)"},"value":{"type":"string","description":"Secret value (will be encrypted at rest)"}}}}}},"responses":{"200":{"description":"Secret created or updated successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Secret"}}}},"400":{"description":"Invalid request body or project ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"},"404":{"description":"Project not found"},"500":{"description":"Internal server error"}},"tags":["Secrets"],"deprecated":true}},"/v1/apps/{appId}/secrets/{secretName}":{"get":{"summary":"Get a secret","description":"Legacy alias of `/v1/projects/{projectId}/secrets/{secretName}`. Projects were previously called apps. Returns a specific secret by name. The secret value is always masked.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"appId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"in":"path","name":"secretName","required":true,"schema":{"type":"string"},"description":"The name of the secret"},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"Secret details (value is masked)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Secret"}}}},"400":{"description":"Invalid project ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope or admin or owner role required"},"404":{"description":"Project or secret not found"},"500":{"description":"Internal server error"}},"tags":["Secrets"],"deprecated":true},"delete":{"summary":"Delete a secret","description":"Legacy alias of `/v1/projects/{projectId}/secrets/{secretName}`. Projects were previously called apps. Permanently deletes a secret by name.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"appId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"in":"path","name":"secretName","required":true,"schema":{"type":"string"},"description":"The name of the secret"},{"$ref":"#/components/parameters/OrgName"}],"responses":{"204":{"description":"Secret deleted successfully"},"400":{"description":"Invalid project ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"},"404":{"description":"Project or secret not found"},"500":{"description":"Internal server error"}},"tags":["Secrets"],"deprecated":true}},"/v1/apps/{appId}/org-secrets":{"get":{"summary":"List the organization secrets linked to a project","description":"Legacy alias of `/v1/projects/{projectId}/org-secrets`. Projects were previously called apps. Returns the organization secrets this project receives at deploy time. Values are always masked. `shadowed` is true when a project secret has the same name; the project secret is the one delivered.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"appId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"The linked organization secrets, masked","content":{"application/json":{"schema":{"type":"object","properties":{"org_secrets":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/Secret"},{"type":"object","properties":{"shadowed":{"type":"boolean"}}}]}}}}}}},"400":{"description":"Invalid project ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope (requires secrets:read) or the viewer role"},"404":{"description":"Project not found"},"500":{"description":"Internal server error"}},"tags":["Secrets"],"deprecated":true}},"/v1/apps/{appId}/org-secrets/{secretName}":{"put":{"summary":"Link an organization secret to a project","description":"Legacy alias of `/v1/projects/{projectId}/org-secrets/{secretName}`. Projects were previously called apps. Makes the named organization secret available to the project at deploy time. Linking a secret that is already linked changes nothing. The project's own secrets are not changed; a project secret with the same name takes precedence.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"appId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"in":"path","name":"secretName","required":true,"schema":{"type":"string"},"description":"The name of the organization secret"},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"The secret was already linked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Secret"}}}},"201":{"description":"The secret was linked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Secret"}}}},"400":{"description":"Invalid project ID or secret name"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope (requires secrets:write) or the viewer role"},"404":{"description":"Project or organization secret not found"},"409":{"description":"The project already links the maximum of 1000 organization secrets (code: `org_secret_link_limit`)"},"429":{"description":"Another link change is in progress for this project. Try again."},"500":{"description":"Internal server error"},"503":{"description":"Failed to link the organization secret. Try again."}},"tags":["Secrets"],"deprecated":true},"delete":{"summary":"Unlink an organization secret from a project","description":"Legacy alias of `/v1/projects/{projectId}/org-secrets/{secretName}`. Projects were previously called apps. Stops delivering the named organization secret to the project from its next deploy. The secret itself is kept.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"appId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"in":"path","name":"secretName","required":true,"schema":{"type":"string"},"description":"The name of the organization secret"},{"$ref":"#/components/parameters/OrgName"}],"responses":{"204":{"description":"The secret was unlinked"},"400":{"description":"Invalid project ID or secret name"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope (requires secrets:write) or the viewer role"},"404":{"description":"Project or organization secret not found, or the secret is not linked to the project"},"429":{"description":"Another link change is in progress for this project. Try again."},"500":{"description":"Internal server error"},"503":{"description":"Failed to unlink the organization secret. Try again."}},"tags":["Secrets"],"deprecated":true}},"/v1/apps/{appId}/api-keys":{"get":{"summary":"Get API keys","description":"Legacy alias of `/v1/projects/{projectId}/api-keys`. Projects were previously called apps. Returns the API keys for a specific project. API keys are used to authenticate requests to the Spice.ai runtime.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"appId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"API keys for the project","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeys"}}}},"400":{"description":"Invalid project ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope or viewer role cannot regenerate API keys"},"404":{"description":"Project not found"},"500":{"description":"Internal server error"}},"tags":["API Keys"],"deprecated":true},"post":{"summary":"Regenerate an API key","description":"Legacy alias of `/v1/projects/{projectId}/api-keys`. Projects were previously called apps. Regenerates an API key for the specified project. This invalidates the previous key. Use key_number to specify which key to regenerate (0 for both, 1 for primary, 2 for secondary).","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"appId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"$ref":"#/components/parameters/OrgName"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"key_number":{"type":"integer","enum":[0,1,2],"default":1,"description":"Which API key to regenerate (0 = both, 1 = primary, 2 = secondary)"}}}}}},"responses":{"200":{"description":"API key regenerated successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeysRegenerated"}}}},"400":{"description":"Invalid request body or project ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"},"404":{"description":"Project not found"},"500":{"description":"Internal server error"}},"tags":["API Keys"],"deprecated":true}},"/v1/apps/{appId}/monitors":{"get":{"summary":"List project monitors","description":"Legacy alias of `/v1/projects/{projectId}/monitors`. Projects were previously called apps. Returns the project's monitors. Requires `monitors:read`. Monitors on a project that is not managed, or on a template that is not available to the organization, are omitted. An existing `container_cpu` monitor without an effective CPU limit remains visible with `evaluation_unavailable_reason` set to `cpu_limit_missing`.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"appId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"List of project monitors","content":{"application/json":{"schema":{"type":"object","properties":{"monitors":{"type":"array","items":{"$ref":"#/components/schemas/ProjectMonitor"}}}}}}},"400":{"description":"Invalid project ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"},"404":{"description":"Project not found"},"500":{"description":"Internal server error"}},"tags":["Monitors"],"deprecated":true},"post":{"summary":"Create a project monitor","description":"Legacy alias of `/v1/projects/{projectId}/monitors`. Projects were previously called apps. Creates a project monitor from a runtime template. Requires `monitors:write`. A monitor evaluates a runtime metric on a schedule and fires when the comparison holds; to fire on matching dataset rows instead, create a data reaction at `/v1/projects/{projectId}/reactions`.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"appId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"$ref":"#/components/parameters/OrgName"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","templateId","spec"],"properties":{"name":{"type":"string","description":"Display name. Unique, case-insensitively, among the project's live monitors and data reactions. A collision answers 409.","minLength":1,"maxLength":100},"description":{"type":"string","maxLength":500},"kind":{"type":"string","description":"Optional assertion that `templateId` names a monitor. A value that disagrees with the template is refused rather than ignored; omit it and the template decides.","enum":["runtime"]},"templateId":{"type":"string","description":"The runtime template the monitor evaluates. `query_latency_p95` is retained for existing monitors but cannot be used to create new ones; use `query_latency_p99`. `container_cpu` is available only on managed projects with a defined CPU limit. A data-reaction template is refused here. Create those at `/v1/projects/{projectId}/reactions`.","enum":["query_failures","query_latency_p95","query_latency_p99","llm_failures","dataset_refresh_errors","http_5xx","flight_failures","memory_working_set","container_cpu","dataset_status","instance_health"]},"spec":{"type":"object","description":"Monitor parameters. Which properties apply is decided by `templateId`. Each property below names the templates that read it, and a property no monitor template reads is stored rather than rejected. `spec` is shared with data reactions, so it accepts their properties too; `model` is the one that is refused rather than stored.","required":["op","threshold"],"properties":{"op":{"type":"string","description":"Comparison applied to the metric. Required on every monitor spec. `instance_health` requires `GT`.","enum":["GT","GEQ","LT","LEQ","EQ","NEQ"]},"threshold":{"type":"number","description":"Value compared against the metric. HTTP failure monitors count estimated 5xx responses in the selected window. Rate-based conditions use responses per second. `dataset_refresh_errors` counts refresh errors in the window. `query_latency_p99` uses milliseconds. `memory_working_set` uses percent of the instance memory limit. A stored memory threshold above 1000 uses bytes. `container_cpu` uses percent of the defined CPU limit. `dataset_status` compares load-state numbers. `instance_health` requires 0. Every monitor spec requires a threshold."},"window":{"type":"string","description":"Range evaluated by `query_failures`, `query_latency_p99`, `llm_failures`, `dataset_refresh_errors`, `http_5xx`, `flight_failures`, and `container_cpu`. `memory_working_set` and `dataset_status` read the current value and ignore this field. `instance_health` reads current failure states and uses a fixed five-minute eviction observation window, independent of this field. Data reactions store this field but do not use it.","default":"5m","enum":["1m","5m","15m","30m","1h"]},"sustainSecs":{"type":"integer","description":"Sustain delay in seconds. Failure monitors default to 0. Other monitors default to 300. Explicit values are preserved.","minimum":0,"maximum":86400},"severity":{"type":"string","description":"Severity the monitor fires at. Recorded on each monitor event and carried into the email or webhook notification.","default":"critical","enum":["warn","critical"]},"dataset":{"type":"string","description":"Read by `dataset_status`, which watches this one dataset. Omit it to match any dataset in the project.","minLength":1,"maxLength":128,"pattern":"^(?!.*\\.\\.)[A-Za-z_][A-Za-z0-9_.-]*$"},"includeDetails":{"type":"boolean","description":"When true, the recorded event and the notification may carry a `details` string quoted from the firing payload when it contains useful information. Off by default, because that payload may be sensitive. A notification's own summary, description, and observed value do not depend on it."},"value":{"description":"Read by `dataset_status` for notification wording only, and only when `threshold` is not one of the state numbers above and this names a state. It never changes what the monitor evaluates. Otherwise a data-reaction property.","oneOf":[{"type":"string","maxLength":256},{"type":"number"}]},"column":{"type":"string","description":"A data-reaction property that no monitor evaluates. Leave it unset. A monitor carrying both `column` and `dataset` has its notification describe that row predicate instead of its own metric comparison.","minLength":1,"maxLength":64,"pattern":"^[A-Za-z_][A-Za-z0-9_]*$"},"datasets":{"type":"array","description":"A data-reaction property that no monitor reads.","maxItems":20,"items":{"type":"string","minLength":1,"maxLength":128,"pattern":"^(?!.*\\.\\.)[A-Za-z_][A-Za-z0-9_.-]*$"}},"conditions":{"type":"array","description":"A data-reaction property that no monitor evaluates. Leave it unset. A monitor carrying conditions has its notification describe them instead of its own metric comparison. IS_NULL and IS_NOT_NULL omit value.","maxItems":20,"items":{"type":"object","required":["dataset","column","op"],"properties":{"dataset":{"type":"string","minLength":1,"maxLength":128,"pattern":"^(?!.*\\.\\.)[A-Za-z_][A-Za-z0-9_.-]*$"},"column":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[A-Za-z_][A-Za-z0-9_]*$"},"op":{"type":"string","enum":["EQ","NEQ","GT","GEQ","LT","LEQ","CONTAINS","STARTS_WITH","ENDS_WITH","IS_NULL","IS_NOT_NULL"]},"value":{"description":"Required except for IS_NULL and IS_NOT_NULL.","oneOf":[{"type":"string","minLength":1,"maxLength":256},{"type":"number"}]}}}},"returnFields":{"$ref":"#/components/schemas/ReactionReturnFields"},"queryLanguage":{"type":"string","description":"A data-reaction property that no monitor reads.","enum":["cypher","gql"]},"query":{"type":"string","description":"A data-reaction property that no monitor reads.","maxLength":10000},"model":{"type":"object","description":"Model transform. Data reactions only. Sending it with a monitor template is refused with 400, not ignored.","required":["model","instructions"],"properties":{"model":{"type":"string","minLength":1,"maxLength":128},"instructions":{"type":"string","minLength":1,"maxLength":4000}}}}},"target":{"description":"Notification sink. Omit it to email the user the credential belongs to; a machine credential has no user, so its monitors email the organization owner.","$ref":"#/components/schemas/ClusterMonitorTarget"},"targets":{"type":"array","minItems":1,"maxItems":3,"description":"Full destination set. When present, this replaces `target` and is the complete list. At most one sink of each type.","items":{"$ref":"#/components/schemas/ClusterMonitorTarget"}},"recipientUserIds":{"type":"array","description":"Legacy spelling of `target.recipientUserIds`, read only when an email target omits its own.","maxItems":50,"items":{"type":"integer","minimum":1}}}}}}},"responses":{"201":{"description":"Monitor created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectMonitor"}}}},"400":{"description":"Invalid request body or project ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"},"404":{"description":"Project not found, or the template is not available for this project (`code` is `monitor_template_unavailable`). Monitors run only on managed projects, and only released templates are available unless the organization has early access."},"409":{"description":"A live monitor or data reaction in this project already has this name"},"413":{"description":"Request body too large"},"422":{"description":"Slack is not connected for the organization"},"500":{"description":"Internal server error"},"502":{"description":"Failed to provision monitor"}},"tags":["Monitors"],"deprecated":true}},"/v1/apps/{appId}/monitors/{alertId}":{"get":{"summary":"Get a project monitor","description":"Legacy alias of `/v1/projects/{projectId}/monitors/{alertId}`. Projects were previously called apps. Returns a single project monitor. Requires `monitors:read`. An existing `container_cpu` monitor without an effective CPU limit remains visible with `evaluation_unavailable_reason` set to `cpu_limit_missing`.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"appId","required":true,"schema":{"type":"integer"}},{"in":"path","name":"alertId","required":true,"schema":{"type":"string","format":"uuid"}},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"Monitor","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectMonitor"}}}},"400":{"description":"Invalid project or monitor ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"},"404":{"description":"Project or monitor not found"},"500":{"description":"Internal server error"}},"tags":["Monitors"],"deprecated":true},"patch":{"summary":"Update a project monitor","description":"Legacy alias of `/v1/projects/{projectId}/monitors/{alertId}`. Projects were previously called apps. Updates a project monitor. Requires `monitors:write` and organization admin privileges.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"appId","required":true,"schema":{"type":"integer"}},{"in":"path","name":"alertId","required":true,"schema":{"type":"string","format":"uuid"}},{"$ref":"#/components/parameters/OrgName"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"description":{"type":["string","null"],"maxLength":500},"status":{"type":"string","enum":["active","disabled"]},"templateId":{"type":"string"},"spec":{"type":"object"},"target":{"description":"Omit to keep the existing destination.","$ref":"#/components/schemas/ClusterMonitorTarget"},"recipientUserIds":{"type":"array","items":{"type":"integer"}}}}}}},"responses":{"200":{"description":"Monitor updated"},"400":{"description":"Invalid request or monitor ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope or role"},"404":{"description":"Project or monitor not found"},"409":{"description":"Duplicate monitor name or monitor changed during update"},"413":{"description":"Request body too large"},"422":{"description":"Destination is unavailable or project configuration is incomplete"},"500":{"description":"Internal server error"},"502":{"description":"Monitor backend update failed"}},"tags":["Monitors"],"deprecated":true},"delete":{"summary":"Delete a project monitor","description":"Legacy alias of `/v1/projects/{projectId}/monitors/{alertId}`. Projects were previously called apps. Soft-deletes a project monitor and tears down the monitor it provisioned. Requires `monitors:write`.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"appId","required":true,"schema":{"type":"integer"}},{"in":"path","name":"alertId","required":true,"schema":{"type":"string","format":"uuid"}},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"Monitor deleted"},"400":{"description":"Invalid project or monitor ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"},"404":{"description":"Project or monitor not found"},"500":{"description":"Internal server error"},"502":{"description":"Failed to delete monitor"}},"tags":["Monitors"],"deprecated":true}},"/v1/apps/{appId}/reactions":{"get":{"summary":"List data reactions","description":"Legacy alias of `/v1/projects/{projectId}/reactions`. Projects were previously called apps. Returns Drasi data reactions for the specified project. Requires `reactions:read`.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"appId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"List of data reactions","content":{"application/json":{"schema":{"type":"object","properties":{"reactions":{"type":"array","items":{"$ref":"#/components/schemas/Reaction"}}}}}}},"400":{"description":"Invalid project ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"},"404":{"description":"Project not found"},"500":{"description":"Internal server error"}},"tags":["Reactions"],"deprecated":true},"post":{"summary":"Create a data reaction","description":"Legacy alias of `/v1/projects/{projectId}/reactions`. Projects were previously called apps. Creates a Drasi data reaction from a data template. Requires `reactions:write`. Email and HTTP are sinks. Optional `spec.model` transforms the matching query result with a spicepod model that can tool-call back to Cloud to send the configured sink.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"appId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"$ref":"#/components/parameters/OrgName"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","templateId","spec"],"properties":{"name":{"type":"string","description":"Display name. Unique, case-insensitively, among the project's live monitors and data reactions. A collision answers 409."},"description":{"type":"string"},"templateId":{"type":"string"},"spec":{"type":"object","description":"Reaction parameters. Which properties apply is decided by `templateId`. Each property below names the templates that read it, and a property a template does not read is accepted and ignored rather than rejected.","required":["op","threshold"],"properties":{"op":{"type":"string","description":"Comparison operator. Required on every reaction spec.","enum":["GT","GEQ","LT","LEQ","EQ","NEQ"]},"threshold":{"type":"number","description":"Numeric threshold. Required on every reaction spec. `task_history_slow` compares it against the task duration in milliseconds; `dataset_row_match` and `dataset_query` match on the query itself, so they conventionally send 0."},"window":{"type":"string","description":"Evaluation window. Only project monitors (`/v1/projects/{projectId}/monitors`) act on it. A data reaction fires on the matching row, so the value is stored and never read.","default":"5m","enum":["1m","5m","15m","30m","1h"]},"sustainSecs":{"type":"integer","description":"How long the condition must hold before firing, in seconds. Like `window`, only project monitors act on it.","default":300,"minimum":0,"maximum":86400},"severity":{"type":"string","description":"Severity recorded on each reaction event and carried into the email or webhook notification.","default":"critical","enum":["warn","critical"]},"dataset":{"type":"string","description":"Single dataset to watch. Required by `dataset_row_match` when `conditions` is omitted.","maxLength":128},"column":{"type":"string","description":"Column to compare. Required by `dataset_row_match` when `conditions` is omitted.","maxLength":64},"value":{"description":"Value to compare `column` against. Required by `dataset_row_match` when `conditions` is omitted.","oneOf":[{"type":"string","maxLength":256},{"type":"number"}]},"datasets":{"type":"array","description":"Datasets the reaction watches. `dataset_row_match` uses this list. Omit it to derive names from `conditions`. `dataset_query` does not read this field.","maxItems":20,"items":{"type":"string","maxLength":128}},"conditions":{"type":"array","description":"Row-match predicates, read by `dataset_row_match`. CONTAINS, STARTS_WITH, ENDS_WITH, IS_NULL, and IS_NOT_NULL are allowed in addition to equality and ordering. IS_NULL and IS_NOT_NULL omit value.","maxItems":20,"items":{"type":"object","required":["dataset","column","op"],"properties":{"dataset":{"type":"string"},"column":{"type":"string"},"op":{"type":"string","enum":["EQ","NEQ","GT","GEQ","LT","LEQ","CONTAINS","STARTS_WITH","ENDS_WITH","IS_NULL","IS_NOT_NULL"]},"value":{"description":"Required except for IS_NULL and IS_NOT_NULL.","oneOf":[{"type":"string"},{"type":"number"}]}}}},"returnFields":{"$ref":"#/components/schemas/ReactionReturnFields"},"queryLanguage":{"type":"string","description":"Language of `query`, read by `dataset_query`. Defaults to `gql` when omitted.","default":"gql","enum":["cypher","gql"]},"query":{"type":"string","description":"Authored continuous query. Required by `dataset_query`.","maxLength":10000},"includeDetails":{"type":"boolean","description":"When true, notifications include matching row details. Off by default."},"model":{"type":"object","description":"Optional spicepod model transform (data reactions only). The model can call one tool for each selected sink (send_email, send_webhook, or send_slack).","required":["model","instructions"],"properties":{"model":{"type":"string","description":"Spicepod model or worker name"},"instructions":{"type":"string","description":"System instructions sent with each matching query result"}}}}},"target":{"description":"Reaction sink. Omit to email the caller.","$ref":"#/components/schemas/ClusterMonitorTarget"},"targets":{"type":"array","minItems":1,"maxItems":3,"description":"Full destination set. When present, this replaces `target` and is the complete list. At most one sink of each type.","items":{"$ref":"#/components/schemas/ClusterMonitorTarget"}}}}}}},"responses":{"201":{"description":"Reaction created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Reaction"}}}},"400":{"description":"Invalid request body or project ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"},"404":{"description":"Project not found"},"409":{"description":"A live monitor or data reaction in this project already has this name"},"413":{"description":"Request body too large"},"422":{"description":"A required project capability or Slack connection is unavailable"},"500":{"description":"Internal server error"},"502":{"description":"Failed to provision reaction"},"503":{"description":"The project's Drasi data source is starting or unavailable"}},"tags":["Reactions"],"deprecated":true}},"/v1/apps/{appId}/reactions/{alertId}":{"get":{"summary":"Get a data reaction","description":"Legacy alias of `/v1/projects/{projectId}/reactions/{alertId}`. Projects were previously called apps. Returns a single data reaction. Requires `reactions:read`.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"appId","required":true,"schema":{"type":"integer"}},{"in":"path","name":"alertId","required":true,"schema":{"type":"string","format":"uuid"}},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"Reaction","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Reaction"}}}},"400":{"description":"Invalid project or reaction ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"},"404":{"description":"Project or reaction not found"},"500":{"description":"Internal server error"}},"tags":["Reactions"],"deprecated":true},"patch":{"summary":"Update a data reaction","description":"Legacy alias of `/v1/projects/{projectId}/reactions/{alertId}`. Projects were previously called apps. Updates reaction fields supplied in the request. Requires `reactions:write` and organization membership.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"appId","required":true,"schema":{"type":"integer"}},{"in":"path","name":"alertId","required":true,"schema":{"type":"string","format":"uuid"}},{"$ref":"#/components/parameters/OrgName"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"description":{"type":["string","null"],"maxLength":500},"status":{"type":"string","enum":["active","disabled"]},"templateId":{"type":"string"},"spec":{"type":"object"},"target":{"$ref":"#/components/schemas/ClusterMonitorTarget"},"targets":{"type":"array","minItems":1,"maxItems":3,"items":{"$ref":"#/components/schemas/ClusterMonitorTarget"}},"recipientUserIds":{"type":"array","items":{"type":"integer"}}}}}}},"responses":{"200":{"description":"Reaction updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Reaction"}}}},"400":{"description":"Invalid request, specification, or dataset"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope or organization membership"},"404":{"description":"Project or reaction not found"},"409":{"description":"Name conflict or concurrent update"},"413":{"description":"Request body too large"},"422":{"description":"Project configuration is incomplete"},"429":{"description":"Another update is in progress for this reaction"},"500":{"description":"Internal server error"},"502":{"description":"Drasi update failed"},"503":{"description":"The project's Drasi data source or lock store is unavailable"}},"tags":["Reactions"],"deprecated":true},"delete":{"summary":"Delete a data reaction","description":"Legacy alias of `/v1/projects/{projectId}/reactions/{alertId}`. Projects were previously called apps. Soft-deletes a data reaction and tears down its Drasi query. Requires `reactions:write`.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"appId","required":true,"schema":{"type":"integer"}},{"in":"path","name":"alertId","required":true,"schema":{"type":"string","format":"uuid"}},{"$ref":"#/components/parameters/OrgName"}],"responses":{"200":{"description":"Reaction deleted"},"400":{"description":"Invalid project or reaction ID"},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope"},"404":{"description":"Project or reaction not found"},"500":{"description":"Internal server error"},"502":{"description":"Failed to delete reaction"}},"tags":["Reactions"],"deprecated":true}},"/v1/orgs/{orgId}/projects":{"get":{"summary":"List projects","description":"The org is `orgId` in the path. Returns projects belonging to the authenticated organization. Page with `limit` (default 1000, max 1000). Prefer `after_id` for a stable keyset (descending `id`). `offset` is accepted for compatibility. PostgREST also caps a page at 1000 rows.\n","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":1000,"default":1000},"description":"Maximum number of projects to return"},{"in":"query","name":"offset","schema":{"type":"integer","minimum":0,"default":0},"description":"Number of projects to skip when a cursor is not supplied"},{"in":"query","name":"after_id","schema":{"type":"integer","minimum":1},"description":"Return projects with a strictly smaller id than this value."},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"responses":{"200":{"description":"List of projects","content":{"application/json":{"schema":{"type":"object","properties":{"projects":{"type":"array","items":{"$ref":"#/components/schemas/Project"}}}}}}},"400":{"description":"Invalid limit, offset, or after_id. orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."}},"tags":["Projects"]},"post":{"summary":"Create a project","description":"The org is `orgId` in the path. Creates a new project in the authenticated organization.","security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":4,"pattern":"^[a-zA-Z0-9-]+$","description":"Project name (letters, numbers, hyphens only)"},"region":{"type":"string","enum":["us-east-1","us-west-2"],"description":"Cloud region where a managed project will be deployed (e.g. `us-east-1`). Omit this, `cname`, and `cluster_name` to create an unattached standalone project whose placement is resolved when linked.\n"},"cname":{"type":"string","description":"**Deprecated.** Internal region CNAME (e.g. `us-east-1-prod-aws-data`). Use `region` instead. Accepted for backwards compatibility when `region` is omitted. Omit all region sources to create an unattached standalone project.\n"},"cluster_name":{"type":"string","description":"Name of the dedicated cluster to deploy to, as listed by `GET /v1/clusters`. Provide exactly one of `region` or `cluster_name`. When set, the region/routing are derived from the cluster; if `region` is also provided it must match the cluster's region. Omit all region sources to create an unattached standalone project.\n"},"description":{"type":"string","description":"Project description"},"visibility":{"type":"string","enum":["public","private"],"default":"private"},"default_alerts":{"type":"boolean","default":true,"description":"Create default monitoring alerts for the new managed project. False skips initial setup. Manual alert creation remains available."},"tags":{"type":"object","additionalProperties":{"type":"string"},"description":"Key-value tags for the project"},"update_channel":{"type":"string","enum":["stable","preview","nightly"],"description":"Update channel for a managed project deployment. A standalone project has no spicepod until linking, so configure its channel after linking."},"replicas":{"type":"integer","minimum":0,"description":"Number of replicas for a managed project. Rejected for standalone projects."},"resources":{"type":"object","description":"Resource requests and limits for a managed project container. Rejected for standalone projects.","properties":{"limits":{"type":"object","properties":{"cpu":{"type":"string","description":"Whole-number vCPU limit, or '-' for no CPU limit"},"memory":{"type":"string","description":"Memory limit in Gi (for example, 16Gi)"},"ephemeral-storage":{"type":"string","description":"Ephemeral storage limit in Gi (for example, 8Gi)"}}},"requests":{"type":"object","properties":{"cpu":{"type":"string","description":"CPU request. Minimum 100 millicores (for example, 100m or 0.1)."}}}}},"executor":{"type":"object","description":"Executor container configuration for a managed project. Rejected for standalone projects.","properties":{"replicas":{"type":"integer","minimum":0,"description":"Number of executor replicas"},"resources":{"type":"object","description":"Resource requests and limits for the executor container","properties":{"limits":{"type":"object","properties":{"cpu":{"type":"string","description":"Whole-number vCPU limit, or '-' for no CPU limit"},"memory":{"type":"string","description":"Memory limit in Gi (for example, 16Gi)"},"ephemeral-storage":{"type":"string","description":"Ephemeral storage limit in Gi (for example, 8Gi)"}}},"requests":{"type":"object","properties":{"cpu":{"type":"string","description":"CPU request. Minimum 100 millicores (for example, 100m or 0.1)."}}}}}}}}}}}},"responses":{"201":{"description":"Project created successfully. The response may include a `warning` field if initial configuration could not be applied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectWithConfig"}}}},"400":{"description":"Invalid request body or invalid region. orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope, dedicated-cluster assignment or special-org private-compute ceilings required for resource limits, or requested resources exceed dedicated-cluster capacity. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Organization not found, including an organization deleted while the project was being created"},"409":{"description":"A project with this name already exists, or the project was deleted while its initial configuration was being written (`code` is `project_deleted`)"},"429":{"description":"Too many project creation requests (rate limited)"},"500":{"description":"Project creation failed, including when the name-collision check could not be read","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"Generic failure reason; the underlying cause stays in the server logs","example":"Failed to create project"}}}}}}},"tags":["Projects"],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}]}},"/v1/orgs/{orgId}/projects/{projectId}":{"get":{"summary":"Get a project","description":"The org is `orgId` in the path. Returns details for a specific project, including its configuration.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"responses":{"200":{"description":"Project details with configuration","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"},"kind":{"type":"string","enum":["managed","standalone","byoc"],"description":"Resolved project hosting model"},"description":{"type":"string"},"visibility":{"type":"string","enum":["public","private"]},"created_at":{"type":"string","format":"date-time"},"cluster_name":{"type":["string","null"],"description":"Resolved dedicated cluster name; null for projects not assigned to a dedicated cluster."},"endpoint":{"type":["string","null"],"description":"The data-plane endpoint the project connects to, or null before a standalone instance is attached."},"production_branch":{"type":"string"},"api_key":{"type":"string","deprecated":true,"description":"Deprecated. Use the API Keys endpoints instead."},"tags":{"type":"object","additionalProperties":{"type":"string"}},"forked_from":{"type":["object","null"],"description":"The project this project was forked from, while that project is live in the same organization. Null for a project that was not created as a fork.\n","required":["id","name"],"properties":{"id":{"type":"integer","description":"ID of the source project"},"name":{"type":"string","description":"Name of the source project"}}},"paused_at":{"type":["string","null"],"format":"date-time","description":"Timestamp when the project was paused; null while running"},"config":{"type":"object","properties":{"spicepod":{"type":"object"},"image_tag":{"type":"string","description":"Runtime image tag"},"update_channel":{"type":["string","null"],"enum":["stable","preview","nightly",null],"description":"Update channel for the runtime, or null when Spice operates this project's runtime updates."},"version":{"type":"string","description":"Semver range for the runtime version (e.g. 1.x, 2.x)"},"replicas":{"type":"integer"},"resources":{"type":["object","null"],"description":"Resource requests and limits for the project container","properties":{"limits":{"type":"object","properties":{"cpu":{"type":"string","description":"Whole-number vCPU limit, or '-' for no CPU limit"},"memory":{"type":"string","description":"Memory limit in Gi (for example, 16Gi)"},"ephemeral-storage":{"type":"string"}}},"requests":{"type":"object","properties":{"cpu":{"type":"string","description":"CPU request. Minimum 100 millicores (for example, 100m or 0.1)."}}}}},"executor":{"type":["object","null"],"description":"Executor container configuration","properties":{"replicas":{"type":"integer","description":"Number of executor replicas"},"resources":{"type":"object","description":"Resource requests and limits for the executor container","properties":{"limits":{"type":"object","properties":{"cpu":{"type":"string","description":"Whole-number vCPU limit, or '-' for no CPU limit"},"memory":{"type":"string","description":"Memory limit in Gi (for example, 16Gi)"},"ephemeral-storage":{"type":"string"}}},"requests":{"type":"object","properties":{"cpu":{"type":"string","description":"CPU request. Minimum 100 millicores (for example, 100m or 0.1)."}}}}},"storage_size_gb":{"type":["number","null"]}}},"region":{"type":["string","null"],"description":"Server-resolved region; null before a standalone instance is attached."},"storage_size_gb":{"type":["number","null"]},"storage_claim_size_gb":{"type":"number","deprecated":true,"description":"Deprecated. Use storage_size_gb instead."}}}}}}}},"400":{"description":"orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Project not found"},"500":{"description":"Internal server error"}},"tags":["Projects"]},"put":{"summary":"Update a project","description":"The org is `orgId` in the path. Updates a project's metadata and configuration, including description, visibility, spicepod configuration, replicas, and resource limits.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"description":{"type":"string"},"visibility":{"type":"string","enum":["public","private"]},"production_branch":{"type":"string"},"tags":{"type":"object","additionalProperties":{"type":"string"},"description":"Key-value tags for the project. The map replaces the stored tags. System tags the project already has stay on the project when the map omits them.\n"},"spicepod":{"oneOf":[{"type":"string","description":"YAML string of the spicepod configuration"},{"type":"object","description":"JSON object of the spicepod configuration"}],"description":"The project's Spicepod. A project connected to a GitHub repository takes its Spicepod from the repository's spicepod.yaml, so for such a project only the stored Spicepod, sent back unchanged, is accepted."},"image_tag":{"type":["string","null"],"description":"Runtime image tag for a managed project; rejected for standalone projects. A non-empty value pins the project. On stable, the pin must be a published stable runtime version. On preview, it must be a published preview or stable runtime version. On stable or preview, send null to clear a pin so deploys resolve from the channel catalog (stable - latest GA for the configured version range; preview - latest RC). null is rejected on nightly and internal channels."},"update_channel":{"type":"string","enum":["stable","preview","nightly"],"description":"Update channel for a managed runtime; rejected for standalone projects."},"version":{"type":"string","description":"Semver range for a managed runtime version (e.g. 1.x, 2.x); rejected for standalone projects."},"replicas":{"type":"integer","minimum":0,"description":"Managed runtime replica count; rejected for standalone projects."},"resources":{"type":"object","description":"Resource requests and limits for a managed project container; rejected for standalone projects.","properties":{"limits":{"type":"object","properties":{"cpu":{"type":"string","description":"Whole-number vCPU limit, or '-' for no CPU limit"},"memory":{"type":"string","description":"Memory limit in Gi (for example, 16Gi)"},"ephemeral-storage":{"type":"string","description":"Ephemeral storage limit in Gi (for example, 8Gi)"}}},"requests":{"type":"object","properties":{"cpu":{"type":"string","description":"CPU request. Minimum 100 millicores (for example, 100m or 0.1)."}}}}},"executor":{"type":"object","description":"Executor container configuration for a managed project; rejected for standalone projects.","properties":{"replicas":{"type":"integer","minimum":0,"description":"Number of executor replicas"},"resources":{"type":"object","description":"Resource requests and limits for the executor container","properties":{"limits":{"type":"object","properties":{"cpu":{"type":"string","description":"Whole-number vCPU limit, or '-' for no CPU limit"},"memory":{"type":"string","description":"Memory limit in Gi (for example, 16Gi)"},"ephemeral-storage":{"type":"string","description":"Ephemeral storage limit in Gi (for example, 8Gi)"}}},"requests":{"type":"object","properties":{"cpu":{"type":"string","description":"CPU request. Minimum 100 millicores (for example, 100m or 0.1)."}}}}},"storage_size_gb":{"type":"number"}}},"region":{"type":"string","description":"Managed project region; standalone placement is resolved during attachment."},"cluster_name":{"type":"string","description":"Reassign the project to a dedicated cluster, as listed by `GET /v1/clusters`. Provide at most one of `region` or `cluster_name`. When set, region/routing are derived from the cluster; if `region` is also provided it must match the cluster's region. Rejected for standalone projects, whose placement is resolved during attachment.\n"},"storage_size_gb":{"type":"number","description":"Managed runtime storage size; rejected for standalone projects."},"storage_claim_size_gb":{"type":"number","deprecated":true,"description":"Deprecated. Use storage_size_gb instead. Rejected for standalone projects."}}}}}},"responses":{"200":{"description":"Project updated successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectWithConfig"}}}},"400":{"description":"Invalid request body or project ID, including placement or hosted runtime fields that do not apply to standalone projects. When `image_tag` is supplied on stable or preview, a tag that is not a published version for that channel returns `{ error: 'Invalid image_tag for stable channel', code: 'invalid_stable_image_tag' }` (or the preview analog). A `spicepod` that adds a component on a source the Spice runtime no longer loads returns code `retired_component_source`; components the project already stores on such a source may be kept, edited and renamed, and a project backed by a GitHub repository is not checked. A `spicepod` that adds a plain-text secret (a password, token, API key, private key, or URL password in a component param) returns code `plaintext_secrets` with `plaintext_secrets` listing each path. Store the value as a project or org secret and use `${secrets:NAME}` in its place. Secrets the stored spicepod already holds do not block the update. orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope, viewer role cannot mutate projects, requested replicas or resources exceed plan limits, dedicated-cluster assignment or special-org private-compute ceilings required, requested resources exceed dedicated-cluster capacity, `image_tag` was supplied outside the Enterprise plan (code: `image_tag_requires_enterprise`), or the requested runtime image is not available for the organization plan (code: `image_tag_not_allowed_for_plan`); omitting `image_tag` leaves the configured image unchanged. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Project not found"},"409":{"description":"The project's lifecycle state changed while the update was in flight (a pause, resume, or restore landed at the same time), so the configuration was not saved (code: `project_state_changed`). Re-read the project before retrying: a configuration update clears `paused_at`, so repeating this request against a project that has since been paused also resumes it. Or a `spicepod` that differs from the stored one was sent for a project connected to a GitHub repository (code: `github_connected`); change spicepod.yaml in the repository instead, since nothing in that request is applied."},"500":{"description":"Internal server error"},"502":{"description":"The configuration was stored but could not be published to the spicepod registry (code: `registry_publish_failed`), so the project still deploys its previously published configuration. Retry the request."},"503":{"description":"Runtime version catalog is unavailable"}},"tags":["Projects"]},"delete":{"summary":"Delete a project","description":"The org is `orgId` in the path. Deletes a project and tears down its runtime resources.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project to delete"},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"responses":{"204":{"description":"Project deleted successfully"},"400":{"description":"Invalid project ID. orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope or viewer role cannot mutate projects. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Project not found"},"500":{"description":"Internal server error"}},"tags":["Projects"]}},"/v1/orgs/{orgId}/projects/{projectId}/forks":{"get":{"summary":"List forks of a project","description":"The org is `orgId` in the path. Returns the live projects in the organization that were forked from this project, ordered by name. Requires `apps:read`.\n","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the source project"},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"responses":{"200":{"description":"Forks of the project","content":{"application/json":{"schema":{"type":"object","required":["forks"],"properties":{"forks":{"type":"array","items":{"$ref":"#/components/schemas/Project"}}}}}}},"400":{"description":"Invalid project ID. orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Project not found"},"500":{"description":"Internal server error"}},"tags":["Projects"]},"post":{"summary":"Fork a project","description":"The org is `orgId` in the path. Creates a new project from this project's configuration and records this project as its source. The fork copies the spicepod the project deploys, its linked connections, project secrets, linked organization secrets, update channel, version range, replica and executor counts, storage size, description, and tags. It does not copy API keys (new ones are generated), deployments, the GitHub repository link, the instance size, or visibility (forks are private). The fork is not deployed: deploy it with POST /v1/projects/{projectId}/deployments. Requires `apps:write`. The server copies secret values without returning them, so no secrets scope is needed. A source connected to a GitHub repository is read from that repository, and its spicepod is copied only when it loads no other file from the repository; to deploy a fork from a fork of the repository, fork the project in the Spice portal.\n","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project to fork"},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"name":{"type":"string","minLength":4,"maxLength":38,"pattern":"^[a-zA-Z0-9-]+$","description":"Name of the fork. Defaults to the source name with `-fork`, then `-fork-2` and so on, whichever is free.\n"},"region":{"type":"string","enum":["us-east-1","us-west-2"],"description":"Region to place the fork in. Omit it and `cluster_name` to place the fork where the source runs.\n"},"cluster_name":{"type":"string","description":"Dedicated cluster to place the fork on, as listed by `GET /v1/clusters`. If `region` is also set, it must match the cluster's region.\n"},"scheduler_state_location":{"type":"string","maxLength":2048,"description":"s3:// URI for the fork's scheduler state. Required when the source spicepod sets `runtime.scheduler.state_location`, and must not overlap it: two schedulers cannot share a location.\n"},"default_alerts":{"type":"boolean","default":true,"description":"Create default monitoring alerts for the new managed fork. False skips initial setup. Manual alert creation remains available."}}}}}},"responses":{"201":{"description":"Project forked. It has not been deployed yet.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ProjectWithConfig"},{"type":"object","required":["shared_state"],"properties":{"shared_state":{"type":"array","description":"Settings copied as they are that name external state the source also uses: a replication slot or Kafka consumer group the spicepod names, or a snapshot location. Change them in the fork's spicepod before deploying it if the two projects should not share that state.\n","items":{"type":"object","required":["component","setting","value"],"properties":{"component":{"type":"string","description":"The dataset, or `snapshots`"},"setting":{"type":"string","example":"pg_replication_slot"},"value":{"type":"string"}}}}}}]}}}},"400":{"description":"Invalid project ID or request body, an unknown region or cluster, no `region` or `cluster_name` set for a source whose placement a fork cannot reuse (a BYOC cluster, a self-hosted runtime, or a region or cluster that is no longer available; `code: fork_placement_required`), or a missing or unusable `scheduler_state_location` (`code: scheduler_state_location_required`, `scheduler_state_location_unsupported`, `scheduler_state_location_overlaps_source`, or `scheduler_state_location_not_applicable`). orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"402":{"description":"The source runs models on hosted AI credits, which the organization has used up (`code: ai_credits_exhausted`).\n"},"403":{"description":"Insufficient scope, a viewer role, or the source runs more replicas or executors than the plan or the dedicated cluster allows. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Project or organization not found"},"409":{"description":"A project with the requested name already exists, another project creation is in progress, or every default name is taken (`code: fork_name_unavailable`)\n"},"413":{"description":"Request body too large"},"422":{"description":"The source has no spicepod (`code: fork_source_has_no_spicepod`), its spicepod is not valid (`code: fork_source_spicepod_invalid`), or its spicepod loads files from its GitHub repository besides spicepod.yaml (`code: fork_source_loads_repository_files`)\n"},"429":{"description":"Too many project creation requests (rate limited)"},"500":{"description":"The fork could not be created. When the failure comes after the project row is created, the fork is deleted again with its spicepod, secrets, and organization secret links. If the project itself cannot be deleted, the response has `code: fork_rollback_failed` and names the partially created project in `project` (`id` and `name`). Delete that project, by `project.id`, before reusing its name.\n"},"502":{"description":"The source is connected to a GitHub repository, and its spicepod could not be read from it (`code: fork_source_repository_unreadable`)\n"}},"tags":["Projects"]}},"/v1/orgs/{orgId}/projects/{projectId}/pause":{"post":{"summary":"Pause a project","description":"The org is `orgId` in the path. Pauses the project's running spicepod. The runtime is torn down and the\nspicepod record is marked as paused. Settings and configuration are\npreserved. Resume with POST /v1/projects/{projectId}/resume. Requires\n`apps:write`.\n","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project to pause"},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"responses":{"200":{"description":"Project paused","content":{"application/json":{"schema":{"type":"object","required":["id","name","paused_at"],"properties":{"id":{"type":"integer","description":"The project ID"},"name":{"type":"string","description":"The project name"},"paused_at":{"type":"string","format":"date-time","description":"Timestamp when the project was paused"}}}}}},"400":{"description":"Invalid project ID, missing API key, no spicepod configured, or spicepod has been deleted. orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Project not found"},"409":{"description":"Project is already paused","content":{"application/json":{"schema":{"type":"object","required":["error","paused_at"],"properties":{"error":{"type":"string"},"paused_at":{"type":"string","format":"date-time","description":"Timestamp of the existing pause"}}}}}},"500":{"description":"Internal server error"}},"tags":["Projects"]}},"/v1/orgs/{orgId}/projects/{projectId}/resume":{"post":{"summary":"Resume a paused project","description":"The org is `orgId` in the path. Clears the paused state on the project's spicepod and creates a new\ndeployment to bring the runtime back up. A project connected to a\nGitHub repository deploys spicepod.yaml from its root directory on its\nproduction branch, read when the project resumes. Requires\n`apps:write`.\n","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project to resume"},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"responses":{"200":{"description":"Project resumed, deployment created","content":{"application/json":{"schema":{"type":"object","required":["id","name","paused_at","deployment_id"],"properties":{"id":{"type":"integer","description":"The project ID"},"name":{"type":"string","description":"The project name"},"paused_at":{"type":["string","null"],"format":"date-time","description":"Always null after a successful resume"},"deployment_id":{"type":"integer","description":"ID of the deployment created to bring the runtime back up"}}}}}},"400":{"description":"Invalid project ID, missing API key, missing project organization, no spicepod configured, spicepod has been deleted, or the spicepod `name`, `kind` or `version` is missing or not valid (the body carries an `issues` array of `{path, message}`). For a project connected to a GitHub repository, also returned when the root directory has no spicepod.yaml or it is invalid or too large (`code: spicepod_not_found`, `spicepod_invalid` or `spicepod_too_large`). The project stays paused. orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope, or the org's GitHub App installation cannot read the connected repository (`code: github_unauthorized` or `github_forbidden`). Also returned when the installation was removed (`code: github_installation_removed`) or suspended (`code: github_installation_suspended`), or when it cannot read the connected repository because the repository is outside its repository access (`code: github_repository_unreachable`). The project stays paused. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Project not found, or the connected GitHub repository or its production branch was not found (`code: github_not_found`). The project stays paused.\n"},"409":{"description":"Project is not paused, or its spicepod was deleted after the request read it. Also returned when another deployment of the project is queued (`code: deployment_in_progress`); the project then stays paused. Also returned when the project's repository, root directory, or production branch changes while the spicepod is read from the repository; the project then stays paused. Resume again.\n"},"422":{"description":"No runtime version in the catalog matches this project’s version range (`code: version_no_match`), or the spicepod uses version v1beta1, which the Spice 2 runtime cannot load (`code: spicepod_version_unsupported`). Also returned when the spicepod uses a connection credential that holds a secret reference deploy cannot resolve (`code: connection_credentials_unresolved`, with an `unresolved_credentials` array of `{connection, field}`). The project stays paused.\n"},"429":{"description":"GitHub rate-limited the read of the connected repository (`code: github_rate_limited`). The project stays paused.\n"},"500":{"description":"Internal server error"},"502":{"description":"GitHub did not answer while the connected repository was read (`code: github_unavailable`). The project stays paused.\n"},"503":{"description":"The runtime version catalog is unavailable or empty (`code: version_source_unavailable`), so an unpinned stable or preview resume cannot resolve an image tag.\n"}},"tags":["Projects"]}},"/v1/orgs/{orgId}/projects/{projectId}/repository":{"put":{"summary":"Connect a project to a GitHub repository","description":"The org is `orgId` in the path. Connects the project to a repository in the organization's GitHub\naccount, through the organization's Spice Cloud GitHub App\ninstallation. The next deployment reads spicepod.yaml (or\nspicepod.yml) from `root_directory` on `production_branch`.\nConnecting sets the project description to the repository\ndescription. Connecting again replaces the repository, root directory\nand production branch.\nRequires `apps:write`.\n","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project to connect"},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["repository"],"properties":{"repository":{"type":"string","description":"Repository in the organization's GitHub account, as `name` or `owner/name`. The owner must be the organization's GitHub account.","example":"cookbook"},"root_directory":{"type":["string","null"],"maxLength":512,"description":"Directory inside the repository that contains spicepod.yaml. Omit it, or send null or an empty string, for the repository root.","example":"cayenne"},"production_branch":{"type":"string","maxLength":255,"description":"Branch that deployments read. Defaults to the repository's default branch. A branch that GitHub renamed is stored under its new name.","example":"trunk"}}}}}},"responses":{"200":{"description":"Project connected","content":{"application/json":{"schema":{"type":"object","required":["project_id","repository","repository_id","root_directory","production_branch","connected_at","has_spicepod"],"properties":{"project_id":{"type":"integer","description":"The project ID"},"repository":{"type":"string","description":"The repository as `owner/name`"},"repository_id":{"type":"integer","description":"The GitHub repository ID"},"root_directory":{"type":["string","null"],"description":"The stored root directory. Null is the repository root."},"production_branch":{"type":"string","description":"The branch that deployments read"},"connected_at":{"type":"string","format":"date-time","description":"When the project was connected"},"has_spicepod":{"type":"boolean","description":"True when spicepod.yaml or spicepod.yml exists in the root directory on the production branch. A deployment fails until it does."}}}}}},"400":{"description":"Invalid project ID or request body, a repository outside the organization's GitHub account (code: `invalid_repository`), or a root directory outside the repository (code: `invalid_root_directory`). orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope, or viewer role cannot mutate projects. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Project not found, or the organization's GitHub App installation cannot read the repository (code: `repository_not_found`)"},"409":{"description":"The organization has no usable GitHub App installation (code: `github_not_connected`), or another project already deploys this repository and root directory (code: `root_directory_conflict`)"},"413":{"description":"Request body too large"},"422":{"description":"The repository has no branch named `production_branch` (code: `branch_not_found`), the project has no spicepod configuration (code: `spicepod_missing`), or the organization is not fully configured"},"500":{"description":"Internal server error"},"503":{"description":"GitHub did not answer (code: `github_unavailable`)"}},"tags":["Projects"]}},"/v1/orgs/{orgId}/projects/{projectId}/metrics":{"get":{"summary":"Get project metrics","description":"The org is `orgId` in the path. Returns current resource utilization metrics (CPU, memory, disk I/O) and data ingestion metrics for a project.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"in":"query","name":"window","required":false,"schema":{"type":"string"},"description":"Prometheus-style duration (e.g. `5m`, `1h`, `30s`). When provided, counter metrics are returned as rates over the window instead of raw cumulative values. For example, `rows_ingested` returns average rows per second with a window, or total cumulative rows without one."},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"responses":{"200":{"description":"Resource and ingestion metrics. Missing fields indicate no data is currently available. Individual metric queries may fail independently; partial results are returned when possible.","content":{"application/json":{"schema":{"type":"object","properties":{"ingestion":{"type":"object","description":"Aggregated data ingestion metrics","properties":{"rows_ingested":{"type":"number","description":"Total rows ingested (without window) or rows per second (with window)"},"bytes_ingested":{"type":"number","description":"Total bytes ingested (without window) or bytes per second (with window)"}}},"cluster":{"type":"object","description":"Cluster metrics for the project","properties":{"active_executors_count":{"type":"number","description":"Number of active executors registered with the scheduler"}}},"metrics":{"type":"object","description":"Resource metrics keyed by pod name","additionalProperties":{"type":"object","properties":{"cpu_usage_percent":{"type":"number","description":"CPU usage as a percentage (0-100)"},"memory_usage_bytes":{"type":"number","description":"Memory usage in bytes"},"disk_read_bytes":{"type":"number","description":"Disk bytes read per second (with window) or cumulative (without window)"},"disk_read_operations":{"type":"number","description":"Disk read operations per second (with window) or cumulative count (without window)"},"disk_write_bytes":{"type":"number","description":"Disk bytes written per second (with window) or cumulative (without window)"},"disk_write_operations":{"type":"number","description":"Disk write operations per second (with window) or cumulative count (without window)"}}}}}}}}},"400":{"description":"Invalid project ID. orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Project or organization not found"},"422":{"description":"Organization is not fully configured"},"500":{"description":"Internal server error"},"503":{"description":"Metrics service unavailable"}},"tags":["Projects"]}},"/v1/orgs/{orgId}/projects/{projectId}/instances":{"get":{"summary":"List project instances","description":"The org is `orgId` in the path. Lists the runtime instances of a deployed project, newest first. A managed or BYOC project lists its Spicepod instances. A standalone project lists its Cloud Connect runtime. Read the logs of an instance with `GET /v1/projects/{projectId}/instances/{instanceName}/logs`.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"responses":{"200":{"description":"The project's instances","content":{"application/json":{"schema":{"type":"object","required":["instances"],"properties":{"instances":{"type":"array","items":{"$ref":"#/components/schemas/ProjectInstance"}}}}}}},"400":{"description":"Invalid project ID. orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Project not found"},"409":{"description":"The project has no running instances. `code` is `project_not_deployed` when a managed or BYOC project was never deployed, or `project_paused` when the project is paused.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectRuntimeError"}}}},"422":{"description":"The project is not fully configured"},"500":{"description":"Internal server error"},"503":{"description":"The project runtime did not answer"}},"tags":["Projects"]}},"/v1/orgs/{orgId}/projects/{projectId}/instances/{instanceName}/logs":{"get":{"summary":"Get project instance logs","description":"The org is `orgId` in the path. Returns the most recent log lines of one instance of a deployed project, oldest first, without color codes. The org's plan sets the largest `tail`. List the instance names with `GET /v1/projects/{projectId}/instances`.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"in":"path","name":"instanceName","required":true,"schema":{"type":"string"},"description":"The instance name from `GET /v1/projects/{projectId}/instances`"},{"in":"query","name":"tail","required":false,"schema":{"type":"integer","minimum":1,"default":100},"description":"How many of the most recent lines to return"},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"responses":{"200":{"description":"The instance's most recent log lines","content":{"application/json":{"schema":{"type":"object","required":["instance","tail","lines"],"properties":{"instance":{"type":"string","description":"The instance name"},"tail":{"type":"integer","description":"The number of lines asked for"},"lines":{"type":"array","items":{"type":"string"},"description":"Log lines, oldest first"}}}}}},"400":{"description":"Invalid project ID or `tail`. orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope (`code` is `insufficient_scope`), or `tail` is larger than the org's plan allows (`code` is `plan_limit`, and `max_tail` is the largest allowed `tail`). The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`).","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"properties":{"error":{"type":"string"},"code":{"type":"string"},"max_tail":{"type":"integer"}}}}}},"404":{"description":"The project was not found, or the instance is not one of the project's instances (`code` is `instance_not_found`)."},"409":{"description":"The logs cannot be read in the project's current state. `code` is `project_not_deployed` when the project was never deployed, `project_paused` when it is paused, or `instance_offline` when the Cloud Connect runtime is offline and does not answer.","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"properties":{"error":{"type":"string"},"code":{"type":"string","enum":["project_not_deployed","project_paused","instance_offline"]}}}}}},"422":{"description":"The project is not fully configured"},"500":{"description":"Internal server error"},"503":{"description":"The project runtime did not answer"}},"tags":["Projects"]}},"/v1/orgs/{orgId}/projects/{projectId}/deployments":{"get":{"summary":"List deployments","description":"The org is `orgId` in the path. Returns a list of deployments for the specified project, ordered by most recent first.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"in":"query","name":"limit","schema":{"type":"integer","default":20},"description":"Maximum number of deployments to return"},{"in":"query","name":"status","schema":{"type":"string","enum":["queued","in_progress","succeeded","failed","created"]},"description":"Filter by deployment status"},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"responses":{"200":{"description":"List of deployments","content":{"application/json":{"schema":{"type":"object","properties":{"deployments":{"type":"array","items":{"$ref":"#/components/schemas/Deployment"}}}}}}},"400":{"description":"orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Project not found"}},"tags":["Deployments"]},"post":{"summary":"Create a deployment","description":"The org is `orgId` in the path. Creates a new deployment for the specified project using its current spicepod configuration. A project connected to a GitHub repository deploys spicepod.yaml from its root directory, read when the deployment is created. It reads the head of the production branch, or the `branch` or `commit_sha` the request names. The runtime image is selected based on the project's update channel.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"image_tag":{"type":"string","description":"Override the runtime image tag for this deployment. Requires the Enterprise plan; omit it to deploy the image the project is configured with. When the effective update channel is `stable` or `preview`, a supplied tag must be a published version for that channel. When the effective update channel is `nightly`, a supplied tag must be a published `spicehq/spiceai` `nightly-*` tag (`YYYYMMDDHHmm` plus `-` plus 7 to 40 lowercase hex characters, for example `202609081124-cadf292`)."},"channel":{"type":"string","enum":["stable","preview","nightly"],"description":"Update channel that determines the runtime image"},"expected_update_channel":{"type":"string","enum":["stable","preview","nightly"],"description":"If set, the stored project update channel must match this value or the request is refused with 409 `update_channel_mismatch`. This is a precondition, not an override (`channel` still changes the image train).\n"},"replicas":{"type":"integer","minimum":1,"maximum":10,"description":"Override the number of replicas for this deployment"},"branch":{"type":"string","minLength":1,"maxLength":255,"description":"For a project connected to a GitHub repository, deploy spicepod.yaml from the head of this branch or tag of that repository instead of the production branch. The branch or tag must exist. With `commit_sha`, it must contain the commit. The production branch and the stored spicepod do not change, so the next deployment without a revision, and any automatic redeploy, deploy the production branch head again. A project with no connected repository refuses this field with 400 `repository_not_connected`. A project whose org has no GitHub App installation refuses it with 422 `github_installation_missing`. A request from the deploy GitHub Action (User-Agent `spice-cloud-deploy-action`) does not check or use this field for any project, because the Action sends the branch of its workflow run as metadata.\n"},"commit_sha":{"type":"string","pattern":"^[0-9a-fA-F]{7,40}$","description":"For a project connected to a GitHub repository, deploy spicepod.yaml from this commit instead of the head of the production branch. The commit must be on `branch`, or on the production branch (never a tag of the same name) when `branch` is omitted, so a commit that only a fork holds is refused. The deployment records the full SHA. The production branch and the stored spicepod do not change. A project with no connected repository refuses this field with 400 `repository_not_connected`. A project whose org has no GitHub App installation refuses it with 422 `github_installation_missing`. A request from the deploy GitHub Action (User-Agent `spice-cloud-deploy-action`) does not check or use this field for any project, because the Action sends the commit of its workflow run as metadata.\n"},"commit_message":{"type":"string","description":"Not used. A deployment from a connected repository records the message of the commit it reads.\n"},"debug":{"type":"boolean","description":"Enable debug mode for this deployment"},"target":{"type":"object","description":"Optional per-deployment override. Omit (or `{ kind: cloud }`) to place the deployment where the project itself runs: Spice-managed compute, its own BYOC cluster, or its attached standalone Cloud Connect instance. That placement is a property of the project, not of this request. Pass `{ kind: cluster, instance_id }` to override that for this deployment and route it to a BYOC cluster connection; the cluster must be owned by the token's org, not in a terminal lifecycle state (decommissioning/archived), not paused, and the operator must hold a live connection (verified by the connectivity ping, not the Supabase lifecycle status). A standalone project cannot be overridden this way and is refused with 409.\n","properties":{"kind":{"type":"string","enum":["cloud","cluster"]},"instance_id":{"type":"string","description":"Required when kind is `cluster`."}}}}}}}},"responses":{"202":{"description":"Deployment created and queued for processing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Deployment"}}}},"400":{"description":"Invalid request body, project has no spicepod configuration, the spicepod has been deleted, or the spicepod is paused (`code: spicepod_paused`; resume it with POST /v1/projects/{projectId}/resume before deploying). Also returned when the spicepod `name`, `kind` or `version` is missing or not valid: the body is `{ error: 'Invalid spicepod configuration' }` with an `issues` array of `{path, message}`. The runtime checks all other spicepod fields when it loads the spicepod. For a standalone Cloud Connect project, also returned when the Spicepod references a `${secrets:...}` name the project does not define: the body carries `code: unknown_secret_references`, a `missing_secrets` array of `{name, path, suggestion?}`, and `secrets_url`. When the effective update channel is `stable` or `preview` and `image_tag` is supplied, the tag must be a published version for that channel or the body is `{ error: 'Invalid image_tag for stable channel', code: 'invalid_stable_image_tag' }` (or the preview analog). When the effective update channel is `nightly` and `image_tag` is supplied, the tag must be a published `spicehq/spiceai` `nightly-*` tag of the form `YYYYMMDDHHmm` plus `-` plus 7 to 40 lowercase hex characters (for example `202609081124-cadf292`) or the body is `{ error: 'Invalid image_tag for nightly channel', code: 'invalid_nightly_image_tag' }`. For a project connected to a GitHub repository, also returned when the root directory has no spicepod.yaml or it is invalid or too large (`code: spicepod_not_found`, `spicepod_invalid` or `spicepod_too_large`), when `commit_sha` is not on `branch` (or on the production branch when `branch` is omitted) (`code: commit_not_on_branch`), or when `commit_sha` comes without `branch` and the project has no production branch (`code: branch_required`). Nothing is dispatched and no deployment is recorded. Also returned when `branch` or `commit_sha` is sent for a project with no connected repository (`code: repository_not_connected`), except from the deploy GitHub Action. Nothing is dispatched. orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope, viewer role cannot create deployments, `image_tag` was supplied outside the Enterprise plan (code: `image_tag_requires_enterprise`), the requested runtime image is not available for the organization plan (code: `image_tag_not_allowed_for_plan`), or the organization's GitHub App installation cannot read the connected repository (code: `github_unauthorized` or `github_forbidden`), was removed (code: `github_installation_removed`), is suspended (code: `github_installation_suspended`), or has no access to the connected repository (code: `github_repository_unreachable`); omitting `image_tag` deploys the image the project is configured with. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Project not found. For a `cluster` target, also returned when the BYOC cluster connection does not exist or is not owned by the token's org. Also returned when the connected GitHub repository, its production branch, or the requested `branch` or `commit_sha` was not found (`code: github_not_found`). With `commit_sha` and no `branch`, this includes a production branch that no longer exists.\n"},"409":{"description":"A deployment is already in progress for this project (`code: deployment_in_progress`, with optional `activeDeploymentId`). For a `cluster` target, also returned when the BYOC cluster is paused, offline, or otherwise not in a deployable state. Also returned when the project's deploy target is unusable: its kind conflicts with its cluster/instance references, a `cluster` target was requested for a standalone project, a standalone project has no attached instance, or that instance has not yet enrolled the encryption key its secrets are sealed to (retrying once it is online succeeds). Also returned when `expected_update_channel` does not match the stored spicepod update channel, or when that channel changes before dispatch (`code: update_channel_mismatch`). Also returned when the project is paused after the request checked it (`code: spicepod_paused`), or its spicepod is deleted after that check (`code: spicepod_deleted`). Also returned when the project's repository, root directory, or production branch changes while the spicepod is read from the repository; deploy again. Nothing is dispatched and no deployment is recorded.\n"},"413":{"description":"Request body exceeds the maximum size (1 MiB), or, for a standalone Cloud Connect project, the project's resolved secrets exceed the maximum sealed-payload size. Nothing is dispatched and no deployment is recorded.\n"},"422":{"description":"Organization is not fully configured, no runtime version in the catalog matches this project’s version range (`code: version_no_match`), or the spicepod uses version v1beta1, which the Spice 2 runtime cannot load (`code: spicepod_version_unsupported`). Also returned when the Spicepod uses a connection credential that holds a secret reference deploy cannot resolve: the body carries `code: connection_credentials_unresolved` and an `unresolved_credentials` array of `{connection, field}`. Nothing is deployed. Also returned when `branch` or `commit_sha` is sent for a project with a connected repository whose org has no GitHub App installation to read it (`code: github_installation_missing`), except from the deploy GitHub Action. Nothing is dispatched.\n"},"429":{"description":"GitHub rate-limited the read of the connected repository (`code: github_rate_limited`).\n"},"500":{"description":"Internal server error. For a standalone Cloud Connect project, also returned with `code: deployment_not_settled` when the deployment record could not be moved out of `queued`; the project's next deployment is refused until it is cleared.\n"},"502":{"description":"Upstream service failure while provisioning the deployment, or, for a standalone Cloud Connect project, the spicepod could not be handed to the instance's gateway, or the instance rejected it. Also returned when GitHub did not answer while the connected repository was read (`code: github_unavailable`).\n"},"503":{"description":"The runtime version catalog is unavailable or empty (`code: version_source_unavailable`), so an unpinned stable or preview deploy cannot resolve an image tag, or catalog membership for an explicit `image_tag` override cannot be validated. Also returned when the published nightly tag list cannot be loaded to validate a nightly `image_tag` override.\n"}},"tags":["Deployments"]}},"/v1/orgs/{orgId}/projects/{projectId}/deployments/{deploymentId}":{"get":{"summary":"Get a deployment","description":"The org is `orgId` in the path. Returns a single deployment for the specified project. The body matches one item from GET /v1/projects/{projectId}/deployments. Not cacheable. Poll this path for stored status after POST create.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"in":"path","name":"deploymentId","required":true,"schema":{"type":"integer"},"description":"The ID of the deployment"},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"responses":{"200":{"description":"Deployment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Deployment"}}}},"400":{"description":"Invalid project ID or deployment ID. orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Project or deployment not found"},"500":{"description":"Internal server error"}},"tags":["Deployments"]}},"/v1/orgs/{orgId}/projects/{projectId}/secrets":{"get":{"summary":"List secrets","description":"The org is `orgId` in the path. Returns all secrets for the specified project. Secret values are always masked.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"responses":{"200":{"description":"List of secrets","content":{"application/json":{"schema":{"type":"object","properties":{"secrets":{"type":"array","items":{"$ref":"#/components/schemas/Secret"}}}}}}},"400":{"description":"Invalid project ID. orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope or viewer role cannot perform write operations. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Project not found"},"500":{"description":"Internal server error"}},"tags":["Secrets"]},"post":{"summary":"Create or update a secret","description":"The org is `orgId` in the path. Creates a new secret or updates an existing secret with the same name. The secret value is encrypted at rest.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","value"],"properties":{"name":{"type":"string","pattern":"^[a-zA-Z_][a-zA-Z0-9_]*$","description":"Secret name (must start with a letter or underscore, alphanumeric and underscores only)"},"value":{"type":"string","description":"Secret value (will be encrypted at rest)"}}}}}},"responses":{"200":{"description":"Secret created or updated successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Secret"}}}},"400":{"description":"Invalid request body or project ID. orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Project not found"},"500":{"description":"Internal server error"}},"tags":["Secrets"]}},"/v1/orgs/{orgId}/projects/{projectId}/secrets/{secretName}":{"get":{"summary":"Get a secret","description":"The org is `orgId` in the path. Returns a specific secret by name. The secret value is always masked.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"in":"path","name":"secretName","required":true,"schema":{"type":"string"},"description":"The name of the secret"},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"responses":{"200":{"description":"Secret details (value is masked)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Secret"}}}},"400":{"description":"Invalid project ID. orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope or admin or owner role required. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Project or secret not found"},"500":{"description":"Internal server error"}},"tags":["Secrets"]},"delete":{"summary":"Delete a secret","description":"The org is `orgId` in the path. Permanently deletes a secret by name.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"in":"path","name":"secretName","required":true,"schema":{"type":"string"},"description":"The name of the secret"},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"responses":{"204":{"description":"Secret deleted successfully"},"400":{"description":"Invalid project ID. orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Project or secret not found"},"500":{"description":"Internal server error"}},"tags":["Secrets"]}},"/v1/orgs/{orgId}/projects/{projectId}/org-secrets":{"get":{"summary":"List the organization secrets linked to a project","description":"The org is `orgId` in the path. Returns the organization secrets this project receives at deploy time. Values are always masked. `shadowed` is true when a project secret has the same name; the project secret is the one delivered.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"responses":{"200":{"description":"The linked organization secrets, masked","content":{"application/json":{"schema":{"type":"object","properties":{"org_secrets":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/Secret"},{"type":"object","properties":{"shadowed":{"type":"boolean"}}}]}}}}}}},"400":{"description":"Invalid project ID. orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope (requires secrets:read) or the viewer role. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Project not found"},"500":{"description":"Internal server error"}},"tags":["Secrets"]}},"/v1/orgs/{orgId}/projects/{projectId}/org-secrets/{secretName}":{"put":{"summary":"Link an organization secret to a project","description":"The org is `orgId` in the path. Makes the named organization secret available to the project at deploy time. Linking a secret that is already linked changes nothing. The project's own secrets are not changed; a project secret with the same name takes precedence.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"in":"path","name":"secretName","required":true,"schema":{"type":"string"},"description":"The name of the organization secret"},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"responses":{"200":{"description":"The secret was already linked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Secret"}}}},"201":{"description":"The secret was linked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Secret"}}}},"400":{"description":"Invalid project ID or secret name. orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope (requires secrets:write) or the viewer role. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Project or organization secret not found"},"409":{"description":"The project already links the maximum of 1000 organization secrets (code: `org_secret_link_limit`)"},"429":{"description":"Another link change is in progress for this project. Try again."},"500":{"description":"Internal server error"},"503":{"description":"Failed to link the organization secret. Try again."}},"tags":["Secrets"]},"delete":{"summary":"Unlink an organization secret from a project","description":"The org is `orgId` in the path. Stops delivering the named organization secret to the project from its next deploy. The secret itself is kept.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"in":"path","name":"secretName","required":true,"schema":{"type":"string"},"description":"The name of the organization secret"},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"responses":{"204":{"description":"The secret was unlinked"},"400":{"description":"Invalid project ID or secret name. orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope (requires secrets:write) or the viewer role. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Project or organization secret not found, or the secret is not linked to the project"},"429":{"description":"Another link change is in progress for this project. Try again."},"500":{"description":"Internal server error"},"503":{"description":"Failed to unlink the organization secret. Try again."}},"tags":["Secrets"]}},"/v1/orgs/{orgId}/projects/{projectId}/api-keys":{"get":{"summary":"Get API keys","description":"The org is `orgId` in the path. Returns the API keys for a specific project. API keys are used to authenticate requests to the Spice.ai runtime.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"responses":{"200":{"description":"API keys for the project","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeys"}}}},"400":{"description":"Invalid project ID. orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope or viewer role cannot regenerate API keys. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Project not found"},"500":{"description":"Internal server error"}},"tags":["API Keys"]},"post":{"summary":"Regenerate an API key","description":"The org is `orgId` in the path. Regenerates an API key for the specified project. This invalidates the previous key. Use key_number to specify which key to regenerate (0 for both, 1 for primary, 2 for secondary).","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"key_number":{"type":"integer","enum":[0,1,2],"default":1,"description":"Which API key to regenerate (0 = both, 1 = primary, 2 = secondary)"}}}}}},"responses":{"200":{"description":"API key regenerated successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeysRegenerated"}}}},"400":{"description":"Invalid request body or project ID. orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Project not found"},"500":{"description":"Internal server error"}},"tags":["API Keys"]}},"/v1/orgs/{orgId}/projects/{projectId}/monitors":{"get":{"summary":"List project monitors","description":"The org is `orgId` in the path. Returns the project's monitors. Requires `monitors:read`. Monitors on a project that is not managed, or on a template that is not available to the organization, are omitted. An existing `container_cpu` monitor without an effective CPU limit remains visible with `evaluation_unavailable_reason` set to `cpu_limit_missing`.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"responses":{"200":{"description":"List of project monitors","content":{"application/json":{"schema":{"type":"object","properties":{"monitors":{"type":"array","items":{"$ref":"#/components/schemas/ProjectMonitor"}}}}}}},"400":{"description":"Invalid project ID. orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Project not found"},"500":{"description":"Internal server error"}},"tags":["Monitors"]},"post":{"summary":"Create a project monitor","description":"The org is `orgId` in the path. Creates a project monitor from a runtime template. Requires `monitors:write`. A monitor evaluates a runtime metric on a schedule and fires when the comparison holds; to fire on matching dataset rows instead, create a data reaction at `/v1/projects/{projectId}/reactions`.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","templateId","spec"],"properties":{"name":{"type":"string","description":"Display name. Unique, case-insensitively, among the project's live monitors and data reactions. A collision answers 409.","minLength":1,"maxLength":100},"description":{"type":"string","maxLength":500},"kind":{"type":"string","description":"Optional assertion that `templateId` names a monitor. A value that disagrees with the template is refused rather than ignored; omit it and the template decides.","enum":["runtime"]},"templateId":{"type":"string","description":"The runtime template the monitor evaluates. `query_latency_p95` is retained for existing monitors but cannot be used to create new ones; use `query_latency_p99`. `container_cpu` is available only on managed projects with a defined CPU limit. A data-reaction template is refused here. Create those at `/v1/projects/{projectId}/reactions`.","enum":["query_failures","query_latency_p95","query_latency_p99","llm_failures","dataset_refresh_errors","http_5xx","flight_failures","memory_working_set","container_cpu","dataset_status","instance_health"]},"spec":{"type":"object","description":"Monitor parameters. Which properties apply is decided by `templateId`. Each property below names the templates that read it, and a property no monitor template reads is stored rather than rejected. `spec` is shared with data reactions, so it accepts their properties too; `model` is the one that is refused rather than stored.","required":["op","threshold"],"properties":{"op":{"type":"string","description":"Comparison applied to the metric. Required on every monitor spec. `instance_health` requires `GT`.","enum":["GT","GEQ","LT","LEQ","EQ","NEQ"]},"threshold":{"type":"number","description":"Value compared against the metric. HTTP failure monitors count estimated 5xx responses in the selected window. Rate-based conditions use responses per second. `dataset_refresh_errors` counts refresh errors in the window. `query_latency_p99` uses milliseconds. `memory_working_set` uses percent of the instance memory limit. A stored memory threshold above 1000 uses bytes. `container_cpu` uses percent of the defined CPU limit. `dataset_status` compares load-state numbers. `instance_health` requires 0. Every monitor spec requires a threshold."},"window":{"type":"string","description":"Range evaluated by `query_failures`, `query_latency_p99`, `llm_failures`, `dataset_refresh_errors`, `http_5xx`, `flight_failures`, and `container_cpu`. `memory_working_set` and `dataset_status` read the current value and ignore this field. `instance_health` reads current failure states and uses a fixed five-minute eviction observation window, independent of this field. Data reactions store this field but do not use it.","default":"5m","enum":["1m","5m","15m","30m","1h"]},"sustainSecs":{"type":"integer","description":"Sustain delay in seconds. Failure monitors default to 0. Other monitors default to 300. Explicit values are preserved.","minimum":0,"maximum":86400},"severity":{"type":"string","description":"Severity the monitor fires at. Recorded on each monitor event and carried into the email or webhook notification.","default":"critical","enum":["warn","critical"]},"dataset":{"type":"string","description":"Read by `dataset_status`, which watches this one dataset. Omit it to match any dataset in the project.","minLength":1,"maxLength":128,"pattern":"^(?!.*\\.\\.)[A-Za-z_][A-Za-z0-9_.-]*$"},"includeDetails":{"type":"boolean","description":"When true, the recorded event and the notification may carry a `details` string quoted from the firing payload when it contains useful information. Off by default, because that payload may be sensitive. A notification's own summary, description, and observed value do not depend on it."},"value":{"description":"Read by `dataset_status` for notification wording only, and only when `threshold` is not one of the state numbers above and this names a state. It never changes what the monitor evaluates. Otherwise a data-reaction property.","oneOf":[{"type":"string","maxLength":256},{"type":"number"}]},"column":{"type":"string","description":"A data-reaction property that no monitor evaluates. Leave it unset. A monitor carrying both `column` and `dataset` has its notification describe that row predicate instead of its own metric comparison.","minLength":1,"maxLength":64,"pattern":"^[A-Za-z_][A-Za-z0-9_]*$"},"datasets":{"type":"array","description":"A data-reaction property that no monitor reads.","maxItems":20,"items":{"type":"string","minLength":1,"maxLength":128,"pattern":"^(?!.*\\.\\.)[A-Za-z_][A-Za-z0-9_.-]*$"}},"conditions":{"type":"array","description":"A data-reaction property that no monitor evaluates. Leave it unset. A monitor carrying conditions has its notification describe them instead of its own metric comparison. IS_NULL and IS_NOT_NULL omit value.","maxItems":20,"items":{"type":"object","required":["dataset","column","op"],"properties":{"dataset":{"type":"string","minLength":1,"maxLength":128,"pattern":"^(?!.*\\.\\.)[A-Za-z_][A-Za-z0-9_.-]*$"},"column":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[A-Za-z_][A-Za-z0-9_]*$"},"op":{"type":"string","enum":["EQ","NEQ","GT","GEQ","LT","LEQ","CONTAINS","STARTS_WITH","ENDS_WITH","IS_NULL","IS_NOT_NULL"]},"value":{"description":"Required except for IS_NULL and IS_NOT_NULL.","oneOf":[{"type":"string","minLength":1,"maxLength":256},{"type":"number"}]}}}},"returnFields":{"$ref":"#/components/schemas/ReactionReturnFields"},"queryLanguage":{"type":"string","description":"A data-reaction property that no monitor reads.","enum":["cypher","gql"]},"query":{"type":"string","description":"A data-reaction property that no monitor reads.","maxLength":10000},"model":{"type":"object","description":"Model transform. Data reactions only. Sending it with a monitor template is refused with 400, not ignored.","required":["model","instructions"],"properties":{"model":{"type":"string","minLength":1,"maxLength":128},"instructions":{"type":"string","minLength":1,"maxLength":4000}}}}},"target":{"description":"Notification sink. Omit it to email the user the credential belongs to; a machine credential has no user, so its monitors email the organization owner.","$ref":"#/components/schemas/ClusterMonitorTarget"},"targets":{"type":"array","minItems":1,"maxItems":3,"description":"Full destination set. When present, this replaces `target` and is the complete list. At most one sink of each type.","items":{"$ref":"#/components/schemas/ClusterMonitorTarget"}},"recipientUserIds":{"type":"array","description":"Legacy spelling of `target.recipientUserIds`, read only when an email target omits its own.","maxItems":50,"items":{"type":"integer","minimum":1}}}}}}},"responses":{"201":{"description":"Monitor created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectMonitor"}}}},"400":{"description":"Invalid request body or project ID. orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Project not found, or the template is not available for this project (`code` is `monitor_template_unavailable`). Monitors run only on managed projects, and only released templates are available unless the organization has early access."},"409":{"description":"A live monitor or data reaction in this project already has this name"},"413":{"description":"Request body too large"},"422":{"description":"Slack is not connected for the organization"},"500":{"description":"Internal server error"},"502":{"description":"Failed to provision monitor"}},"tags":["Monitors"]}},"/v1/orgs/{orgId}/projects/{projectId}/monitors/{alertId}":{"get":{"summary":"Get a project monitor","description":"The org is `orgId` in the path. Returns a single project monitor. Requires `monitors:read`. An existing `container_cpu` monitor without an effective CPU limit remains visible with `evaluation_unavailable_reason` set to `cpu_limit_missing`.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"}},{"in":"path","name":"alertId","required":true,"schema":{"type":"string","format":"uuid"}},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"responses":{"200":{"description":"Monitor","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectMonitor"}}}},"400":{"description":"Invalid project or monitor ID. orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Project or monitor not found"},"500":{"description":"Internal server error"}},"tags":["Monitors"]},"patch":{"summary":"Update a project monitor","description":"The org is `orgId` in the path. Updates a project monitor. Requires `monitors:write` and organization admin privileges.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"}},{"in":"path","name":"alertId","required":true,"schema":{"type":"string","format":"uuid"}},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"description":{"type":["string","null"],"maxLength":500},"status":{"type":"string","enum":["active","disabled"]},"templateId":{"type":"string"},"spec":{"type":"object"},"target":{"description":"Omit to keep the existing destination.","$ref":"#/components/schemas/ClusterMonitorTarget"},"recipientUserIds":{"type":"array","items":{"type":"integer"}}}}}}},"responses":{"200":{"description":"Monitor updated"},"400":{"description":"Invalid request or monitor ID. orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope or role. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Project or monitor not found"},"409":{"description":"Duplicate monitor name or monitor changed during update"},"413":{"description":"Request body too large"},"422":{"description":"Destination is unavailable or project configuration is incomplete"},"500":{"description":"Internal server error"},"502":{"description":"Monitor backend update failed"}},"tags":["Monitors"]},"delete":{"summary":"Delete a project monitor","description":"The org is `orgId` in the path. Soft-deletes a project monitor and tears down the monitor it provisioned. Requires `monitors:write`.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"}},{"in":"path","name":"alertId","required":true,"schema":{"type":"string","format":"uuid"}},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"responses":{"200":{"description":"Monitor deleted"},"400":{"description":"Invalid project or monitor ID. orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Project or monitor not found"},"500":{"description":"Internal server error"},"502":{"description":"Failed to delete monitor"}},"tags":["Monitors"]}},"/v1/orgs/{orgId}/projects/{projectId}/reactions":{"get":{"summary":"List data reactions","description":"The org is `orgId` in the path. Returns Drasi data reactions for the specified project. Requires `reactions:read`.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"responses":{"200":{"description":"List of data reactions","content":{"application/json":{"schema":{"type":"object","properties":{"reactions":{"type":"array","items":{"$ref":"#/components/schemas/Reaction"}}}}}}},"400":{"description":"Invalid project ID. orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Project not found"},"500":{"description":"Internal server error"}},"tags":["Reactions"]},"post":{"summary":"Create a data reaction","description":"The org is `orgId` in the path. Creates a Drasi data reaction from a data template. Requires `reactions:write`. Email and HTTP are sinks. Optional `spec.model` transforms the matching query result with a spicepod model that can tool-call back to Cloud to send the configured sink.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"},"description":"The ID of the project"},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","templateId","spec"],"properties":{"name":{"type":"string","description":"Display name. Unique, case-insensitively, among the project's live monitors and data reactions. A collision answers 409."},"description":{"type":"string"},"templateId":{"type":"string"},"spec":{"type":"object","description":"Reaction parameters. Which properties apply is decided by `templateId`. Each property below names the templates that read it, and a property a template does not read is accepted and ignored rather than rejected.","required":["op","threshold"],"properties":{"op":{"type":"string","description":"Comparison operator. Required on every reaction spec.","enum":["GT","GEQ","LT","LEQ","EQ","NEQ"]},"threshold":{"type":"number","description":"Numeric threshold. Required on every reaction spec. `task_history_slow` compares it against the task duration in milliseconds; `dataset_row_match` and `dataset_query` match on the query itself, so they conventionally send 0."},"window":{"type":"string","description":"Evaluation window. Only project monitors (`/v1/projects/{projectId}/monitors`) act on it. A data reaction fires on the matching row, so the value is stored and never read.","default":"5m","enum":["1m","5m","15m","30m","1h"]},"sustainSecs":{"type":"integer","description":"How long the condition must hold before firing, in seconds. Like `window`, only project monitors act on it.","default":300,"minimum":0,"maximum":86400},"severity":{"type":"string","description":"Severity recorded on each reaction event and carried into the email or webhook notification.","default":"critical","enum":["warn","critical"]},"dataset":{"type":"string","description":"Single dataset to watch. Required by `dataset_row_match` when `conditions` is omitted.","maxLength":128},"column":{"type":"string","description":"Column to compare. Required by `dataset_row_match` when `conditions` is omitted.","maxLength":64},"value":{"description":"Value to compare `column` against. Required by `dataset_row_match` when `conditions` is omitted.","oneOf":[{"type":"string","maxLength":256},{"type":"number"}]},"datasets":{"type":"array","description":"Datasets the reaction watches. `dataset_row_match` uses this list. Omit it to derive names from `conditions`. `dataset_query` does not read this field.","maxItems":20,"items":{"type":"string","maxLength":128}},"conditions":{"type":"array","description":"Row-match predicates, read by `dataset_row_match`. CONTAINS, STARTS_WITH, ENDS_WITH, IS_NULL, and IS_NOT_NULL are allowed in addition to equality and ordering. IS_NULL and IS_NOT_NULL omit value.","maxItems":20,"items":{"type":"object","required":["dataset","column","op"],"properties":{"dataset":{"type":"string"},"column":{"type":"string"},"op":{"type":"string","enum":["EQ","NEQ","GT","GEQ","LT","LEQ","CONTAINS","STARTS_WITH","ENDS_WITH","IS_NULL","IS_NOT_NULL"]},"value":{"description":"Required except for IS_NULL and IS_NOT_NULL.","oneOf":[{"type":"string"},{"type":"number"}]}}}},"returnFields":{"$ref":"#/components/schemas/ReactionReturnFields"},"queryLanguage":{"type":"string","description":"Language of `query`, read by `dataset_query`. Defaults to `gql` when omitted.","default":"gql","enum":["cypher","gql"]},"query":{"type":"string","description":"Authored continuous query. Required by `dataset_query`.","maxLength":10000},"includeDetails":{"type":"boolean","description":"When true, notifications include matching row details. Off by default."},"model":{"type":"object","description":"Optional spicepod model transform (data reactions only). The model can call one tool for each selected sink (send_email, send_webhook, or send_slack).","required":["model","instructions"],"properties":{"model":{"type":"string","description":"Spicepod model or worker name"},"instructions":{"type":"string","description":"System instructions sent with each matching query result"}}}}},"target":{"description":"Reaction sink. Omit to email the caller.","$ref":"#/components/schemas/ClusterMonitorTarget"},"targets":{"type":"array","minItems":1,"maxItems":3,"description":"Full destination set. When present, this replaces `target` and is the complete list. At most one sink of each type.","items":{"$ref":"#/components/schemas/ClusterMonitorTarget"}}}}}}},"responses":{"201":{"description":"Reaction created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Reaction"}}}},"400":{"description":"Invalid request body or project ID. orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Project not found"},"409":{"description":"A live monitor or data reaction in this project already has this name"},"413":{"description":"Request body too large"},"422":{"description":"A required project capability or Slack connection is unavailable"},"500":{"description":"Internal server error"},"502":{"description":"Failed to provision reaction"},"503":{"description":"The project's Drasi data source is starting or unavailable"}},"tags":["Reactions"]}},"/v1/orgs/{orgId}/projects/{projectId}/reactions/{alertId}":{"get":{"summary":"Get a data reaction","description":"The org is `orgId` in the path. Returns a single data reaction. Requires `reactions:read`.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"}},{"in":"path","name":"alertId","required":true,"schema":{"type":"string","format":"uuid"}},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"responses":{"200":{"description":"Reaction","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Reaction"}}}},"400":{"description":"Invalid project or reaction ID. orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Project or reaction not found"},"500":{"description":"Internal server error"}},"tags":["Reactions"]},"patch":{"summary":"Update a data reaction","description":"The org is `orgId` in the path. Updates reaction fields supplied in the request. Requires `reactions:write` and organization membership.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"}},{"in":"path","name":"alertId","required":true,"schema":{"type":"string","format":"uuid"}},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"description":{"type":["string","null"],"maxLength":500},"status":{"type":"string","enum":["active","disabled"]},"templateId":{"type":"string"},"spec":{"type":"object"},"target":{"$ref":"#/components/schemas/ClusterMonitorTarget"},"targets":{"type":"array","minItems":1,"maxItems":3,"items":{"$ref":"#/components/schemas/ClusterMonitorTarget"}},"recipientUserIds":{"type":"array","items":{"type":"integer"}}}}}}},"responses":{"200":{"description":"Reaction updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Reaction"}}}},"400":{"description":"Invalid request, specification, or dataset. orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope or organization membership. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Project or reaction not found"},"409":{"description":"Name conflict or concurrent update"},"413":{"description":"Request body too large"},"422":{"description":"Project configuration is incomplete"},"429":{"description":"Another update is in progress for this reaction"},"500":{"description":"Internal server error"},"502":{"description":"Drasi update failed"},"503":{"description":"The project's Drasi data source or lock store is unavailable"}},"tags":["Reactions"]},"delete":{"summary":"Delete a data reaction","description":"The org is `orgId` in the path. Soft-deletes a data reaction and tears down its Drasi query. Requires `reactions:write`.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"projectId","required":true,"schema":{"type":"integer"}},{"in":"path","name":"alertId","required":true,"schema":{"type":"string","format":"uuid"}},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"responses":{"200":{"description":"Reaction deleted"},"400":{"description":"Invalid project or reaction ID. orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Project or reaction not found"},"500":{"description":"Internal server error"},"502":{"description":"Failed to delete reaction"}},"tags":["Reactions"]}},"/v1/orgs/{orgId}/clusters/{clusterId}/monitors":{"get":{"summary":"List cluster monitors","description":"The org is `orgId` in the path. Lists live monitors for the cluster. Requires `monitors:read` and organization membership. The cluster ID is the `cluster_name` returned by GET /v1/clusters.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"clusterId","required":true,"schema":{"type":"string"}},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"responses":{"200":{"description":"Cluster monitors"},"400":{"description":"Invalid X-Org-Name header. orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope or role. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Cluster not found"},"500":{"description":"Internal server error"}},"tags":["Clusters"]},"post":{"summary":"Create a cluster monitor","description":"The org is `orgId` in the path. Creates and provisions a cluster monitor. Requires `monitors:write` and organization admin privileges.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"clusterId","required":true,"schema":{"type":"string"}},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","templateId","spec"],"properties":{"name":{"type":"string","minLength":1,"maxLength":100},"description":{"type":"string","maxLength":500},"templateId":{"type":"string","enum":["cluster_cpu","cluster_memory","cluster_availability"]},"spec":{"$ref":"#/components/schemas/ClusterMonitorSpec"},"target":{"description":"Omit to email the caller, or the organization owner for a machine credential.","$ref":"#/components/schemas/ClusterMonitorTarget"},"targets":{"type":"array","minItems":1,"maxItems":3,"description":"Full destination set. When present, this replaces `target`. At most one sink of each type.","items":{"$ref":"#/components/schemas/ClusterMonitorTarget"}},"recipientUserIds":{"type":"array","items":{"type":"integer"}}}}}}},"responses":{"201":{"description":"Monitor created"},"400":{"description":"Invalid request body or monitor configuration, or the cluster already has 20 live monitors. orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope or role. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Cluster not found"},"409":{"description":"Duplicate monitor name or monitor deleted during creation","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"properties":{"error":{"type":"string"},"code":{"type":"string","enum":["monitor_name_conflict","monitor_deleted_during_creation"]}}}}}},"413":{"description":"Request body too large"},"422":{"description":"Destination is unavailable"},"500":{"description":"Internal server error"},"502":{"description":"Monitor provisioning or cleanup failed. If cleanup is pending, the response includes the monitor ID for a retry with DELETE."}},"tags":["Clusters"]}},"/v1/orgs/{orgId}/clusters/{clusterId}/monitors/{alertId}":{"get":{"summary":"Get a cluster monitor","description":"The org is `orgId` in the path. Returns a live cluster monitor. Requires `monitors:read` and organization membership.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"clusterId","required":true,"schema":{"type":"string"}},{"in":"path","name":"alertId","required":true,"schema":{"type":"string","format":"uuid"}},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"responses":{"200":{"description":"Monitor"},"400":{"description":"Invalid monitor ID. orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope or role. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Cluster or monitor not found"},"500":{"description":"Internal server error"}},"tags":["Clusters"]},"patch":{"summary":"Update a cluster monitor","description":"The org is `orgId` in the path. Updates the rule, destination, or status and reprovisions the monitor. Requires `monitors:write` and organization admin privileges.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"clusterId","required":true,"schema":{"type":"string"}},{"in":"path","name":"alertId","required":true,"schema":{"type":"string","format":"uuid"}},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"description":{"type":["string","null"],"maxLength":500},"status":{"type":"string","enum":["active","disabled"]},"templateId":{"type":"string","enum":["cluster_cpu","cluster_memory","cluster_availability"]},"spec":{"$ref":"#/components/schemas/ClusterMonitorSpec"},"target":{"description":"Omit to keep the existing destination.","$ref":"#/components/schemas/ClusterMonitorTarget"},"targets":{"type":"array","minItems":1,"maxItems":3,"description":"Full destination set. When present, replaces the stored sinks. Omit to keep them.","items":{"$ref":"#/components/schemas/ClusterMonitorTarget"}},"recipientUserIds":{"type":"array","items":{"type":"integer"}}}}}}},"responses":{"200":{"description":"Monitor updated"},"400":{"description":"Invalid request or monitor ID. orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope or role. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Cluster or monitor not found"},"409":{"description":"Duplicate monitor name or monitor changed during update","content":{"application/json":{"schema":{"type":"object","required":["error","code"],"properties":{"error":{"type":"string"},"code":{"type":"string","enum":["monitor_name_conflict","monitor_changed"]}}}}}},"413":{"description":"Request body too large"},"422":{"description":"Destination is unavailable"},"500":{"description":"Internal server error"},"502":{"description":"Monitor backend update failed; the saved rule may be pending and the request can be retried"}},"tags":["Clusters"]},"delete":{"summary":"Delete a cluster monitor","description":"The org is `orgId` in the path. Soft-deletes the monitor and retires its backend. Requires `monitors:write` and organization admin privileges. Retry to finish pending cleanup.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"clusterId","required":true,"schema":{"type":"string"}},{"in":"path","name":"alertId","required":true,"schema":{"type":"string","format":"uuid"}},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"responses":{"200":{"description":"Monitor deleted"},"400":{"description":"Invalid monitor ID. orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope or role. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Cluster or monitor not found"},"500":{"description":"Internal server error"},"502":{"description":"Backend cleanup failed"}},"tags":["Clusters"]}},"/v1/orgs/{orgId}/members":{"get":{"summary":"List organization members","description":"The org is `orgId` in the path. Returns all members of the authenticated organization.","security":[{"BearerAuth":[]}],"responses":{"200":{"description":"List of organization members","content":{"application/json":{"schema":{"type":"object","properties":{"members":{"type":"array","items":{"type":"object","properties":{"user_id":{"type":"integer"},"username":{"type":"string"},"roles":{"type":"array","items":{"type":"string"}},"is_owner":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"}}}}}}}}},"400":{"description":"orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."}},"tags":["Members"],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}]},"post":{"summary":"Add a member","description":"The org is `orgId` in the path. Adds a new member to the organization with the specified roles.","security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["username"],"properties":{"username":{"type":"string","description":"The username of the user to add"},"roles":{"type":"array","items":{"type":"string"},"description":"Roles to assign to the new member","default":["member"]}}}}}},"responses":{"201":{"description":"Member added successfully","content":{"application/json":{"schema":{"type":"object","properties":{"user_id":{"type":"integer"},"username":{"type":"string"},"roles":{"type":"array","items":{"type":"string"}},"created_at":{"type":"string","format":"date-time"}}}}}},"400":{"description":"Invalid request body. orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope, not an org admin/owner, or only owners can assign the admin or owner role. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"User not found"},"409":{"description":"User is already a member of this organization"}},"tags":["Members"],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}]}},"/v1/orgs/{orgId}/members/{memberId}":{"get":{"summary":"Get a member","description":"The org is `orgId` in the path. Returns details for a specific organization member, including their roles.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"memberId","required":true,"schema":{"type":"integer"},"description":"The user ID of the member"},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"responses":{"200":{"description":"Member details","content":{"application/json":{"schema":{"type":"object","properties":{"user_id":{"type":"integer"},"username":{"type":"string"},"roles":{"type":"array","items":{"type":"string"}},"is_owner":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"}}}}}},"400":{"description":"orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Member not found"}},"tags":["Members"]},"patch":{"summary":"Update a member's roles","description":"The org is `orgId` in the path. Updates the roles assigned to a specific organization member. Organization owners cannot have their roles modified.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"memberId","required":true,"schema":{"type":"integer"},"description":"The user ID of the member"},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["roles"],"properties":{"roles":{"type":"array","items":{"type":"string"},"description":"New roles to assign to the member"}}}}}},"responses":{"200":{"description":"Member roles updated successfully","content":{"application/json":{"schema":{"type":"object","properties":{"user_id":{"type":"integer"},"username":{"type":"string"},"roles":{"type":"array","items":{"type":"string"}},"created_at":{"type":"string","format":"date-time"}}}}}},"400":{"description":"Invalid request body. orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope, not an org admin/owner, cannot modify owner, only owners can modify/assign admin or owner roles. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Member not found, or the membership was removed before the role change was written"},"413":{"description":"Request body too large"},"500":{"description":"The organization could not be read, or the role change failed"}},"tags":["Members"]},"delete":{"summary":"Remove a member","description":"The org is `orgId` in the path. Removes a member from the organization. Organization owners cannot be removed.","security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"path","name":"memberId","required":true,"schema":{"type":"integer"},"description":"The user ID of the member"},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}],"responses":{"204":{"description":"Member removed successfully"},"400":{"description":"Invalid member ID. orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope, not an org admin/owner, cannot remove owner, or only owners can remove admins or owners. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Member not found"},"500":{"description":"The removal could not be completed, or could not be confirmed"}},"tags":["Members"]}},"/v1/orgs/{orgId}/limits":{"get":{"summary":"Get organization limits","description":"The org is `orgId` in the path. Returns the current plan limits for the authenticated organization, including resource limits when private compute is enabled.","security":[{"BearerAuth":[]}],"responses":{"200":{"description":"Organization limits","content":{"application/json":{"schema":{"type":"object","properties":{"apps":{"type":"integer","description":"Maximum number of apps (-1 means unlimited)"},"replicas":{"type":"integer","description":"Maximum replicas per app (-1 means unlimited)"},"spicepods":{"type":"integer","description":"Maximum spicepods per app (-1 means unlimited)"},"users":{"type":"integer","description":"Maximum organization members (-1 means unlimited)"},"api_rps":{"type":"integer","description":"API requests per second limit (-1 means unlimited)"},"request_timeout_secs":{"type":"integer","description":"Request timeout in seconds"},"sql_query_timeout_secs":{"type":"integer","description":"SQL query timeout in seconds"},"sql_concurrent_queries":{"type":"integer","description":"Maximum concurrent SQL queries (-1 means unlimited)"},"resources":{"type":["object","null"],"description":"Resource limits for private compute; `null` when private compute is not enabled","properties":{"max_cpu":{"type":"integer","description":"Maximum vCPUs per container"},"max_memory_gi":{"type":"integer","description":"Maximum memory in Gi per container"}}}}}}}},"400":{"description":"orgId is not a valid org id (`invalid_org_id`), or X-Org-Name does not name that org (`org_path_mismatch`)."},"401":{"description":"Unauthorized"},"403":{"description":"Insufficient scope. The caller is not a member of the org (`org_forbidden`). A machine credential named a different org (`org_assertion_mismatch`). The org is deleted (`org_deleted`)."},"404":{"description":"Organization not found"}},"tags":["Limits"],"parameters":[{"in":"path","name":"orgId","required":true,"schema":{"type":"integer","minimum":1},"description":"The org id from GET /v1/orgs"},{"in":"header","name":"X-Org-Name","required":false,"description":"When present, X-Org-Name must name the same org as orgId. Otherwise the request returns org_path_mismatch (400).","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}]}}},"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"An OAuth 2.0 client-credentials access token, a personal access token, or a portal session token (a Supabase Auth ES256 access token, verified against the issuer's JWKS and refused once its session is signed out).\n"}},"parameters":{"OrgName":{"in":"header","name":"X-Org-Name","required":false,"description":"The organization to act on, by handle. Omit it to act on the organization the credential is bound to; sending it blank is a 400, not the same as omitting it. A user credential may name any organization its owner belongs to; a machine credential is pinned to its own. A portal session token is bound to none, so it must send this header on every route but `GET /v1/orgs` (400 `org_assertion_required` otherwise).\n","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{1,255}$"}}},"schemas":{"Project":{"type":"object","properties":{"id":{"type":"integer","description":"Unique identifier for the project"},"name":{"type":"string","description":"Name of the project"},"kind":{"type":"string","enum":["managed","standalone","byoc"],"description":"Resolved project hosting model"},"description":{"type":["string","null"],"description":"Description of the project"},"visibility":{"type":"string","enum":["public","private"],"description":"Visibility setting for the project"},"created_at":{"type":"string","format":"date-time","description":"Timestamp when the project was created"},"cname":{"type":["string","null"],"description":"Region identifier"},"cluster_name":{"type":["string","null"],"description":"Resolved dedicated cluster name; null for projects not assigned to a dedicated cluster."},"endpoint":{"type":["string","null"],"description":"The data-plane endpoint the project connects to, or null before a standalone instance is attached."},"tags":{"type":["object","null"],"additionalProperties":{"type":"string"},"description":"Key-value tags for the project"}}},"ProjectWithConfig":{"type":"object","properties":{"id":{"type":"integer","description":"Unique identifier for the project"},"name":{"type":"string","description":"Name of the project"},"kind":{"type":"string","enum":["managed","standalone","byoc"],"description":"Resolved project hosting model"},"description":{"type":["string","null"],"description":"Description of the project"},"visibility":{"type":"string","enum":["public","private"],"description":"Visibility setting for the project"},"created_at":{"type":"string","format":"date-time","description":"Timestamp when the project was created"},"cname":{"type":["string","null"],"description":"Region identifier"},"cluster_name":{"type":["string","null"],"description":"Resolved dedicated cluster name; null for projects not assigned to a dedicated cluster."},"endpoint":{"type":["string","null"],"description":"The data-plane endpoint the project connects to, or null before a standalone instance is attached."},"production_branch":{"type":["string","null"],"description":"Production branch for the project"},"api_key":{"type":"string","deprecated":true,"description":"Deprecated. Use the API Keys endpoints instead."},"tags":{"type":["object","null"],"additionalProperties":{"type":"string"},"description":"Key-value tags for the project"},"forked_from":{"type":["object","null"],"description":"The project this project was forked from, while that project is live in the same organization. Null for a project that was not created as a fork.\n","required":["id","name"],"properties":{"id":{"type":"integer","description":"ID of the source project"},"name":{"type":"string","description":"Name of the source project"}}},"paused_at":{"type":["string","null"],"format":"date-time","description":"Timestamp when the project was paused; null while running"},"warning":{"type":["string","null"],"description":"Present when the project was created but initial configuration could not be applied"},"config":{"type":["object","null"],"description":"Runtime configuration; null for an unattached standalone project.","properties":{"spicepod":{"type":["object","null"],"description":"Spicepod configuration"},"registry":{"type":["string","null"],"description":"Container registry for the runtime image"},"image_tag":{"type":["string","null"],"description":"Runtime image tag"},"update_channel":{"type":["string","null"],"enum":["stable","preview","nightly",null],"description":"Update channel for the runtime"},"replicas":{"type":"integer","description":"Number of replicas"},"resources":{"type":["object","null"],"description":"Resource requests and limits for the project container","properties":{"limits":{"type":"object","properties":{"cpu":{"type":"string","description":"Whole-number vCPU limit, or '-' for no CPU limit"},"memory":{"type":"string","description":"Memory limit in Gi (for example, 16Gi)"},"ephemeral-storage":{"type":"string"}}},"requests":{"type":"object","properties":{"cpu":{"type":"string","description":"CPU request. Minimum 100 millicores (for example, 100m or 0.1)."}}}}},"executor":{"type":["object","null"],"description":"Executor container configuration","properties":{"replicas":{"type":"integer","description":"Number of executor replicas"},"resources":{"type":"object","description":"Resource requests and limits for the executor container","properties":{"limits":{"type":"object","properties":{"cpu":{"type":"string","description":"Whole-number vCPU limit, or '-' for no CPU limit"},"memory":{"type":"string","description":"Memory limit in Gi (for example, 16Gi)"},"ephemeral-storage":{"type":"string"}}},"requests":{"type":"object","properties":{"cpu":{"type":"string","description":"CPU request. Minimum 100 millicores (for example, 100m or 0.1)."}}}}}}},"region":{"type":["string","null"],"description":"Cloud region code (e.g., us-east-1)"},"node_group":{"type":["string","null"],"description":"Node group for scheduling the project runtime"},"storage_claim_size_gb":{"type":["number","null"],"description":"Storage claim size in GB"}}}}},"Secret":{"type":"object","properties":{"id":{"type":"integer","description":"Unique identifier for the secret"},"name":{"type":"string","description":"Name of the secret"},"value":{"type":"string","description":"Always masked with asterisks"},"created_at":{"type":"string","format":"date-time","description":"Timestamp when the secret was created"},"updated_at":{"type":"string","format":"date-time","description":"Timestamp when the secret was last updated"}}},"Deployment":{"type":"object","properties":{"id":{"type":"integer","description":"Unique identifier for the deployment"},"status":{"type":"string","enum":["queued","in_progress","succeeded","failed","created"],"description":"Current status of the deployment"},"created_at":{"type":"string","format":"date-time","description":"Timestamp when the deployment was created"},"updated_at":{"type":["string","null"],"format":"date-time","description":"Timestamp when the deployment was last updated"},"image_tag":{"type":["string","null"],"description":"Runtime image tag used for this deployment"},"replicas":{"type":"integer","description":"Number of replicas"},"branch":{"type":["string","null"],"description":"Git branch name"},"commit_sha":{"type":["string","null"],"description":"Git commit SHA"},"commit_message":{"type":["string","null"],"description":"Git commit message"},"error_code":{"type":["string","null"],"description":"Set with error_message on failed and on in_progress while Cloud is retrying a scheduling block. Null on succeeded, on ordinary waiting in_progress, and on writers that predated this column. Older failed rows may still have a message with a null code."},"error_message":{"type":["string","null"],"description":"Catalog copy for the current error_code. Older failed rows may still have a message with a null error_code."},"creation_source":{"type":["string","null"],"description":"Source that triggered the deployment"},"created_by":{"type":["integer","null"],"description":"User ID who created the deployment"}}},"ApiKeys":{"type":"object","properties":{"api_key":{"type":["string","null"],"description":"Primary API key"},"api_key_2":{"type":["string","null"],"description":"Secondary API key (for key rotation)"}}},"ApiKeysRegenerated":{"type":"object","properties":{"api_key":{"type":["string","null"],"description":"Primary API key"},"api_key_2":{"type":["string","null"],"description":"Secondary API key"},"regenerated_key":{"type":"integer","enum":[0,1,2],"description":"Which key was regenerated (0 = both, 1 = primary, 2 = secondary)"}}},"ProjectAlert":{"type":"object","description":"The fields a project monitor and a data reaction share. `Reaction` and `ProjectMonitor` each narrow `kind` and describe their own `spec`.","required":["id","org_id","app_id","created_by","name","description","kind","status","template_id","spec","target","targets","recipient_user_ids","last_fired_at","last_resolved_at","created_at","updated_at"],"properties":{"id":{"type":"string","format":"uuid","description":"Identifier of the monitor or reaction. This is the value the get, update and delete operations take."},"org_id":{"type":"integer","description":"Organization that owns the project"},"app_id":{"type":"integer","description":"Project the monitor or reaction belongs to"},"created_by":{"type":["integer","null"],"description":"User who created it, when it was created by a user credential"},"name":{"type":"string","description":"Display name, unique case-insensitively among the project's live monitors and data reactions"},"description":{"type":["string","null"]},"kind":{"type":"string","enum":["runtime","data"],"description":"Discriminates a project monitor (`runtime`) from a data reaction (`data`)."},"status":{"type":"string","description":"Whether it is evaluating","enum":["active","disabled"]},"template_id":{"type":"string","description":"Template it was created from"},"spec":{"type":"object","description":"Parameters. Which properties are meaningful is decided by `template_id`. The create operation's request body lists the properties each template reads."},"target":{"description":"The primary sink (email, then slack, then http). `targets` is the full list. An HTTP sink's `token` is write-only and is never returned.","$ref":"#/components/schemas/ReactionSinkView"},"targets":{"type":"array","minItems":1,"maxItems":3,"description":"Every configured sink, ordered email, then slack, then http. `target` is the first of these. An HTTP sink's token is never returned.","items":{"$ref":"#/components/schemas/ReactionSinkView"}},"recipient_user_ids":{"type":"array","description":"Organization members emailed when it fires. Empty when there is no email sink, and for an email sink addressed only by `emails`. When a created email sink named no recipients at all, the API stores the creating user.","items":{"type":"integer"}},"last_fired_at":{"type":["string","null"],"format":"date-time","description":"When it last fired, or null if it never has"},"last_resolved_at":{"type":["string","null"],"format":"date-time","description":"When it last resolved, or null if it never has"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"Reaction":{"description":"A data reaction, as returned by the list, get, create and update operations. Every property is always present; a nullable one carries null rather than being omitted. Fields not listed here are internal to the platform and are not part of the contract.","allOf":[{"$ref":"#/components/schemas/ProjectAlert"},{"type":"object","properties":{"kind":{"type":"string","enum":["data"],"description":"The reactions endpoints only ever return `data`."},"spec":{"type":"object","description":"Reaction parameters, normalized for the response: a reaction whose model transform is still held on `target` is returned with that transform copied into `spec.model`. Which properties are meaningful is decided by `template_id`. The create operation's request body lists the properties each template reads."}}}]},"ReactionSinkView":{"description":"A configured sink. An HTTP bearer token is never returned.","discriminator":{"propertyName":"type","mapping":{"email":"#/components/schemas/EmailSinkView","http":"#/components/schemas/HttpSinkView","slack":"#/components/schemas/SlackSinkView"}},"oneOf":[{"$ref":"#/components/schemas/EmailSinkView"},{"$ref":"#/components/schemas/HttpSinkView"},{"$ref":"#/components/schemas/SlackSinkView"}]},"EmailSinkView":{"type":"object","required":["type"],"properties":{"type":{"type":"string","enum":["email"]},"recipientUserIds":{"type":"array","items":{"type":"integer"}},"emails":{"type":"array","items":{"type":"string","format":"email"}}}},"HttpSinkView":{"type":"object","required":["type","url"],"properties":{"type":{"type":"string","enum":["http"]},"url":{"description":"The sink's URL. An empty string means the stored sink no longer passes validation. Update the sink to replace it.","anyOf":[{"type":"string","format":"uri"},{"type":"string","enum":[""]}]},"method":{"type":"string","enum":["POST","PUT","PATCH"]}}},"SlackSinkView":{"type":"object","required":["type","channelId"],"properties":{"type":{"type":"string","enum":["slack"]},"channelId":{"description":"The Slack channel ID. An empty string means the stored sink no longer passes validation. Update the sink to replace it.","anyOf":[{"type":"string","minLength":9,"maxLength":32,"pattern":"^[CGD][A-Z0-9]{8,31}$"},{"type":"string","enum":[""]}]},"channelName":{"type":"string","minLength":1,"maxLength":80}}},"ProjectMonitor":{"description":"A project monitor, as returned by the list, get, create and update operations. Every required property is always present; a nullable one carries null rather than being omitted. `evaluation_unavailable_reason` is present only when an Instance CPU monitor has lost its CPU limit. Fields not listed here are internal to the platform and are not part of the contract.","allOf":[{"$ref":"#/components/schemas/ProjectAlert"},{"type":"object","properties":{"kind":{"type":"string","enum":["runtime"],"description":"The monitors endpoints only ever return `runtime`."},"spec":{"type":"object","description":"Monitor parameters. Which properties are meaningful is decided by `template_id`. The create operation's request body lists the properties each template reads."},"recipient_user_ids":{"type":"array","description":"Organization members emailed when the monitor fires. Empty when there is no email sink, and for an email sink addressed only by `emails`. When a created email sink named no recipients at all, the API stores the creating user; a machine credential has no user, so it stores none and the monitor emails the organization owner.","items":{"type":"integer"}},"evaluation_unavailable_reason":{"type":"string","enum":["cpu_limit_missing"],"description":"Present when an existing Instance CPU monitor cannot evaluate because its CPU limit was removed."}}}]},"ProjectInstance":{"type":"object","required":["name","status","started_at"],"properties":{"name":{"type":"string","description":"The instance name: a Spicepod instance (pod) name, or a Cloud Connect runtime id. Pass it as `instanceName` to read its logs."},"status":{"type":"string","enum":["deploying","loading","ready","unhealthy","terminating","failed","failed_to_start","online","offline"],"description":"A Spicepod instance is `deploying`, `loading`, `ready`, `unhealthy`, `terminating`, `failed`, or `failed_to_start`. A Cloud Connect runtime is `online` or `offline`."},"started_at":{"type":["string","null"],"format":"date-time","description":"When the Spicepod instance started, or when the Cloud Connect runtime enrolled."}}},"ProjectRuntimeError":{"type":"object","required":["error","code"],"properties":{"error":{"type":"string"},"code":{"type":"string","enum":["project_not_deployed","project_paused"]}}},"ReactionReturnFields":{"type":"array","maxItems":20,"description":"Dataset fields returned by generated row-match queries. Include identity and all monitored fields.","items":{"type":"object","required":["dataset","columns"],"properties":{"dataset":{"type":"string"},"columns":{"type":"array","minItems":1,"maxItems":1000,"items":{"type":"string"}}}}},"ClusterMonitorSpec":{"type":"object","required":["op","threshold"],"properties":{"op":{"type":"string","enum":["GT","GEQ","LT","LEQ","EQ","NEQ"]},"threshold":{"type":"number"},"window":{"type":"string","enum":["1m","5m","15m","30m","1h"],"default":"5m"},"sustainSecs":{"type":"integer","minimum":0,"maximum":86400,"default":300},"severity":{"type":"string","enum":["warn","critical"],"default":"critical"},"dataset":{"type":"string","minLength":1,"maxLength":128,"pattern":"^(?!.*\\.\\.)[A-Za-z_][A-Za-z0-9_.-]*$"},"column":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[A-Za-z_][A-Za-z0-9_]*$"},"value":{"oneOf":[{"type":"string","maxLength":256},{"type":"number"}]},"datasets":{"type":"array","maxItems":20,"items":{"type":"string","minLength":1,"maxLength":128,"pattern":"^(?!.*\\.\\.)[A-Za-z_][A-Za-z0-9_.-]*$"}},"conditions":{"type":"array","maxItems":20,"items":{"type":"object","required":["dataset","column","op"],"properties":{"dataset":{"type":"string","minLength":1,"maxLength":128,"pattern":"^(?!.*\\.\\.)[A-Za-z_][A-Za-z0-9_.-]*$"},"column":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[A-Za-z_][A-Za-z0-9_]*$"},"op":{"type":"string","enum":["EQ","NEQ","GT","GEQ","LT","LEQ","CONTAINS","STARTS_WITH","ENDS_WITH","IS_NULL","IS_NOT_NULL"]},"value":{"oneOf":[{"type":"string","minLength":1,"maxLength":256},{"type":"number"}]}}}},"queryLanguage":{"type":"string","enum":["cypher","gql"]},"returnFields":{"$ref":"#/components/schemas/ReactionReturnFields"},"query":{"type":"string","maxLength":10000},"includeDetails":{"type":"boolean"}}},"ClusterMonitorTarget":{"description":"A notification sink in a create or update request. Cluster monitors, project monitors and data reactions accept the same shape.","discriminator":{"propertyName":"type","mapping":{"email":"#/components/schemas/EmailSinkInput","http":"#/components/schemas/HttpSinkInput","slack":"#/components/schemas/SlackSinkInput"}},"oneOf":[{"$ref":"#/components/schemas/EmailSinkInput"},{"$ref":"#/components/schemas/HttpSinkInput"},{"$ref":"#/components/schemas/SlackSinkInput"}]},"EmailSinkInput":{"type":"object","required":["type"],"properties":{"type":{"type":"string","enum":["email"]},"recipientUserIds":{"type":"array","description":"Organization members to email, by user ID.","maxItems":50,"items":{"type":"integer","minimum":1}},"emails":{"type":"array","maxItems":20,"items":{"type":"string","format":"email","minLength":3,"maxLength":254}}}},"HttpSinkInput":{"type":"object","required":["type","url"],"properties":{"type":{"type":"string","enum":["http"]},"url":{"type":"string","format":"uri","minLength":1,"maxLength":2048,"description":"The URL each notification is sent to, using `method`. Must be https and must resolve to a public address."},"method":{"type":"string","enum":["POST","PUT","PATCH"],"description":"A create that omits it uses POST. An update that omits it keeps the stored method."},"token":{"type":"string","minLength":1,"maxLength":512,"description":"Bearer token sent with each delivery. Stored encrypted and never returned."}}},"SlackSinkInput":{"type":"object","required":["type","channelId"],"properties":{"type":{"type":"string","enum":["slack"]},"channelId":{"type":"string","description":"Slack conversation id. The organization must already have Slack connected.","minLength":9,"maxLength":32,"pattern":"^[CGD][A-Z0-9]{8,31}$"},"channelName":{"type":"string","minLength":1,"maxLength":80}}}}},"tags":[{"name":"Health","description":"API health and status"},{"name":"Projects","description":"Manage projects and their configurations"},{"name":"Deployments","description":"Create and monitor project deployments"},{"name":"Secrets","description":"Manage encrypted project secrets"},{"name":"API Keys","description":"Manage runtime API keys for projects"},{"name":"Members","description":"Manage organization members and roles"},{"name":"Regions","description":"Available deployment regions"},{"name":"Limits","description":"Organization plan limits"},{"name":"Documentation","description":"API specification and documentation"},{"name":"Catalogs","description":"Manage managed Iceberg REST catalogs (control plane)"}]}