{
  "openapi": "3.1.0",
  "info": {
    "title": "Unify AI Compute API",
    "version": "v1-draft",
    "description": "Customer-facing contract for the compute job API, mirroring docs/spec/job-api.md. A build with a job store (`DATABASE_URL` set) authenticates every key and runs jobs: submit holds balance, persists the job and launches it on SkyPilot; get, list, cancel, logs, events and artifacts serve it. Which keys it accepts depends on `ACCOUNTS_BACKEND` (docs/spec/accounts.md): `console`, today's production, verifies Unify API `sk-` keys through the console bridge and holds the Unify API balance; `local` accepts only Unify Compute's own `uck_` keys and holds compute's own prepaid balance. A build without one (the dark build) checks only the key's shape and answers every authenticated job route with `501 not_implemented` after validation. Responses marked *Planned* are not returned by any build yet.",
    "license": {
      "name": "Proprietary",
      "identifier": "LicenseRef-Proprietary"
    }
  },
  "servers": [
    {
      "url": "https://staging.compute.unifyai.us",
      "description": "Staging. Defined, not yet applied: this host does not resolve yet."
    },
    {
      "url": "https://api.compute.unifyai.us",
      "description": "Production. Does not exist yet."
    }
  ],
  "tags": [
    {
      "name": "catalogue",
      "description": "Public: what can be bought and where it runs."
    },
    {
      "name": "jobs",
      "description": "Authenticated with a Unify API key."
    },
    {
      "name": "meta",
      "description": "Service metadata and health."
    },
    {
      "name": "internal",
      "description": "Called by a job's own run step with its upload token. Not part of the customer contract."
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/healthz": {
      "get": {
        "operationId": "health",
        "tags": [
          "meta"
        ],
        "security": [],
        "summary": "Liveness and build identity",
        "description": "Operational, not part of the customer contract under /compute/v1; listed so the route table and this document agree.",
        "responses": {
          "200": {
            "description": "Process is serving.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                }
              }
            }
          }
        }
      }
    },
    "/compute/v1/openapi.json": {
      "get": {
        "operationId": "getOpenAPI",
        "tags": [
          "meta"
        ],
        "security": [],
        "summary": "This document",
        "responses": {
          "200": {
            "description": "The OpenAPI 3.1 document embedded in this build.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/compute/v1/skus": {
      "get": {
        "operationId": "listSKUs",
        "tags": [
          "catalogue"
        ],
        "security": [],
        "summary": "List offered SKUs",
        "description": "Public so the pricing page can render from it. Not paginated: the whole catalogue is returned.",
        "responses": {
          "200": {
            "description": "The catalogue.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SKUList"
                }
              }
            }
          }
        }
      }
    },
    "/compute/v1/upstreams": {
      "get": {
        "operationId": "listUpstreams",
        "tags": [
          "catalogue"
        ],
        "security": [],
        "summary": "List upstream clouds",
        "description": "Every cloud the catalogue references, whether this deployment has it enabled, and which SKUs it serves. Sorted by cloud. Public, like /skus.",
        "responses": {
          "200": {
            "description": "Upstreams.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UpstreamList"
                }
              }
            }
          }
        }
      }
    },
    "/compute/v1/templates": {
      "get": {
        "operationId": "listTemplates",
        "tags": [
          "catalogue"
        ],
        "security": [],
        "summary": "List job templates",
        "description": "Public, like /skus. Every curated template (docs/spec/templates.md), including unavailable ones and their reasons, sorted by id. Not paginated.",
        "responses": {
          "200": {
            "description": "The templates.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TemplateList"
                }
              }
            }
          }
        }
      }
    },
    "/compute/v1/jobs": {
      "post": {
        "operationId": "submitJob",
        "tags": [
          "jobs"
        ],
        "summary": "Submit a job",
        "description": "Validated before anything is held or persisted, in this order: body parses (`malformed_body`); if `template` is set, it is offered (`unknown_template`) and available (`template_unavailable`), every field sent is overridable (`field_not_overridable`), env keys are identifiers (`invalid_env`) and inputs bind each declared mount once (`missing_input`, `unknown_input_mount`, `duplicate_input_mount`), and the resolved spec continues through the same checks as a hand-written one: `sku` present (`missing_sku`), offered (`unknown_sku`) and sold (`sku_unavailable`), `spec.max_runtime_seconds` positive (`missing_max_runtime`) and at most the deployment's ceiling, 14400 while the platform starts small and never more than 86400 (`runtime_over_ceiling`); then, in a build with a job store, the spec must be launchable as given (`metadata` at most 16 pairs, `missing_image`, `missing_command`, `invalid_env`, `reserved_env`, `invalid_input_source`, `invalid_mount`, and `501 outputs_unsupported` when the deployment has no artifact storage); every entry of `providers` offered by the SKU (`provider_not_offered`) and at least one candidate enabled in this deployment, with a SkyPilot API server to launch on (`503 upstream_unavailable`). Then, in one transaction per owner: the account's quotas (`403 quota_exceeded`) and the hold on the Unify API balance, `rate × min(max_runtime_seconds, 7200)` (`402 insufficient_balance`; `503 console_unavailable` if the console cannot answer). Only then is the job persisted `queued`, `job.queued` recorded and the job handed to SkyPilot. A refused hold persists nothing. The dark build answers a request that passes the first group of checks with `501 not_implemented`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SubmitJobRequest"
              },
              "example": {
                "sku": "a10g-24gb-x1",
                "providers": [
                  "aws"
                ],
                "spec": {
                  "image": "ghcr.io/acme/embed:1.4",
                  "command": [
                    "python",
                    "run.py"
                  ],
                  "max_runtime_seconds": 7200
                },
                "metadata": {
                  "pipeline": "nightly-embed"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Idempotent replay: the same Idempotency-Key and the same body within 24 h returns the original job, unchanged; nothing is held or launched again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Job"
                }
              }
            }
          },
          "201": {
            "description": "Job accepted: the hold is placed, the job persisted and handed to SkyPilot. `status` reads `provisioning` once the launch is handed over, `queued` if it is still being handed over.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Job"
                }
              }
            }
          },
          "400": {
            "description": "Validation failed. `error.code` is one of `malformed_body`, `missing_sku`, `unknown_sku`, `sku_unavailable`, `missing_max_runtime`, `runtime_over_ceiling`, `provider_not_offered`, with `template` also `unknown_template`, `template_unavailable`, `field_not_overridable`, `invalid_env`, `missing_input`, `unknown_input_mount`, `duplicate_input_mount`, and in a build with a job store also `missing_image`, `missing_command`, `invalid_env`, `reserved_env`, `invalid_input_source`, `invalid_mount`, `invalid_metadata`, `invalid_idempotency_key`; `error.param` names the field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "`insufficient_balance`: the console refused the hold because the balance is below it. `error.message` carries the hold amount and the job's full ceiling (`max_runtime_seconds × rate`). Nothing was persisted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "`quota_exceeded`: the submit would exceed an account quota (docs/spec/quotas.md); refused whole, never a partial grant, `error.quotas` lists each limit with its headroom, no `Retry-After`. Checked after every request error, before the hold, on the resolved SKU. Or `authentication_error`/`account_disabled` or `account_suspended`: the key is valid but its account may not spend. With `ACCOUNTS_BACKEND=local`, before any request check: `permission_error`/`scope_denied`, the key has the `read` scope only; and after the request checks, before quota and the hold: `authentication_error`/`account_suspended` (the account is suspended or closed) or `permission_error`/`compute_not_enabled` (job submission has not been turned on for the account yet; an operator does that by hand while the platform starts small).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                },
                "example": {
                  "error": {
                    "type": "quota_exceeded",
                    "code": "quota_exceeded",
                    "param": "sku",
                    "message": "sku \"a10g-24gb-x1\" would exceed this account's quota (active jobs: 2 of 2 in use, this job needs 1, headroom 0; hyperscaler GPUs: 2 of 2 in use, this job needs 1, headroom 0). Nothing was submitted or held; wait for or cancel an active job, choose a smaller SKU, or ask support to raise the limit.",
                    "quotas": [
                      {
                        "limit": "max_active_jobs",
                        "max": 2,
                        "used": 2,
                        "requested": 1,
                        "headroom": 0
                      },
                      {
                        "limit": "max_gpus",
                        "tier": "hyperscaler",
                        "max": 2,
                        "used": 2,
                        "requested": 1,
                        "headroom": 0
                      }
                    ]
                  }
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict`: the Idempotency-Key was used within 24 h with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "`internal_error`/`placement_failed`: placement failed for a reason other than the two the API distinguishes; `internal_error`/`template_resolution_failed`: a template could not be resolved for a reason that is not the submitter's; `internal_error`/`owner_unresolved` or `quota_check_failed`: the quota check could not be made; `internal_error`/`internal_error`: the job store failed. Not expected in normal operation; nothing was submitted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "501": {
            "description": "The dark build (`not_implemented`), or `outputs_unsupported`: the spec has `outputs` and this deployment has no artifact storage.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "503": {
            "description": "`upstream_unavailable`: no provider the SKU offers (or that `providers` allows) is enabled in this deployment, or the deployment has no SkyPilot API server. `console_unavailable`: the key could not be verified or the hold could not be placed because the console did not answer; nothing was persisted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "listJobs",
        "tags": [
          "jobs"
        ],
        "summary": "List jobs",
        "description": "The calling key's owner's jobs, newest first, by keyset cursor. `next` is present when `has_more` is.",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "style": "form",
            "explode": true,
            "description": "Filter by status; repeatable, any of. An unknown status is `invalid_status`.",
            "schema": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/JobStatus"
              }
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size, 1 to 100 (`invalid_limit`); default 20.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "after",
            "in": "query",
            "required": false,
            "description": "Cursor: the `next` value of the previous page. Not one of your jobs is `invalid_cursor`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of jobs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobList"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_status`, `invalid_limit` or `invalid_cursor`; `error.param` names the parameter.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/AccountRefused"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "`internal_error`: the job store could not be read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "501": {
            "$ref": "#/components/responses/NotImplemented"
          },
          "503": {
            "$ref": "#/components/responses/ConsoleUnavailable"
          }
        }
      }
    },
    "/compute/v1/jobs/{id}": {
      "get": {
        "operationId": "getJob",
        "tags": [
          "jobs"
        ],
        "summary": "Get a job",
        "description": "The job, owner-scoped: another owner's job is `404`, like a missing one.",
        "parameters": [
          {
            "$ref": "#/components/parameters/JobID"
          }
        ],
        "responses": {
          "200": {
            "description": "The job, with `usage` read from the metering ledger.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Job"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/AccountRefused"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "`internal_error`: the job store could not be read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "501": {
            "$ref": "#/components/responses/NotImplemented"
          },
          "503": {
            "$ref": "#/components/responses/ConsoleUnavailable"
          }
        }
      }
    },
    "/compute/v1/jobs/{id}/cancel": {
      "post": {
        "operationId": "cancelJob",
        "tags": [
          "jobs"
        ],
        "summary": "Cancel a job",
        "description": "Requests termination. GPU metering stops at the moment the cancel is accepted, not when the instance is confirmed down: the customer is charged for seconds run, not for the cancel latency. A job that never ran has its hold released in full. Not rate limited.",
        "parameters": [
          {
            "$ref": "#/components/parameters/JobID"
          }
        ],
        "responses": {
          "202": {
            "description": "Cancel accepted. `status` reads `cancelled` once the end is recorded, which is normally before this answer; teardown continues afterwards.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Job"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/AccountRefused"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "`job_terminal`: the job has already ended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "`internal_error`: the cancel could not be recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "501": {
            "$ref": "#/components/responses/NotImplemented"
          },
          "503": {
            "$ref": "#/components/responses/ConsoleUnavailable"
          }
        }
      }
    },
    "/compute/v1/jobs/{id}/logs": {
      "get": {
        "operationId": "getJobLogs",
        "tags": [
          "jobs"
        ],
        "summary": "Job logs",
        "description": "Plain-text stdout/stderr so far, read from the job's cluster. A job not yet launched, or whose cluster has been torn down, has an empty body: logs are not yet kept after teardown (backend.md open question 4), so the 7-day retention in job-api.md is *planned*.",
        "parameters": [
          {
            "$ref": "#/components/parameters/JobID"
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "description": "*Planned.* Not supported yet; ignored.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "follow",
            "in": "query",
            "required": false,
            "description": "`true` would stream as `text/event-stream`; *planned*, answered `501 not_implemented`.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Logs so far (possibly empty).",
            "content": {
              "text/plain; charset=utf-8": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/AccountRefused"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "`internal_error`: with `ACCOUNTS_BACKEND=local`, the key could not be looked up.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "501": {
            "$ref": "#/components/responses/NotImplemented"
          },
          "503": {
            "description": "`upstream_unavailable`/`logs_unavailable`: the cluster's logs could not be read from the SkyPilot API server; or `console_unavailable`: the key could not be verified.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/compute/v1/jobs/{id}/artifacts": {
      "get": {
        "operationId": "listJobArtifacts",
        "tags": [
          "jobs"
        ],
        "summary": "Job artifacts",
        "description": "Files the job wrote under `spec.outputs[].mount`, each with a presigned download URL (ADR-0005, Proposed), owner-scoped like `GET /compute/v1/jobs/{id}`. Answers `501 not_implemented` in the dark build and in a deployment without artifact storage. *Planned:* `402` instead of URLs while the account is in arrears (ADR-0008).",
        "parameters": [
          {
            "$ref": "#/components/parameters/JobID"
          }
        ],
        "responses": {
          "200": {
            "description": "Artifacts.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ArtifactList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/AccountRefused"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "`internal_error`: the job could not be read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "501": {
            "$ref": "#/components/responses/NotImplemented"
          },
          "503": {
            "description": "`console_unavailable`: the key could not be verified; or `storage_unavailable`: artifact storage could not be reached.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/compute/v1/jobs/{id}/events": {
      "get": {
        "operationId": "listJobEvents",
        "tags": [
          "jobs"
        ],
        "summary": "Job events",
        "description": "The job's event log (docs/spec/events.md), oldest first, owner-scoped like `GET /compute/v1/jobs/{id}`. Pass the last id seen as `after` to receive only newer events. Answers `501 not_implemented` in the dark build.",
        "parameters": [
          {
            "$ref": "#/components/parameters/JobID"
          },
          {
            "name": "after",
            "in": "query",
            "required": false,
            "description": "Id of an event of this job; only later events are returned. An id that is not an event of this job is `400 invalid_cursor`.",
            "schema": {
              "type": "string",
              "pattern": "^evt_[0-9A-HJKMNP-TV-Z]{26}$"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size. Outside 1-100 or not an integer is `400 invalid_limit`.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of events.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventList"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_cursor` (`param` is `after`) or `invalid_limit` (`param` is `limit`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/AccountRefused"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "`internal_error`: the event log could not be read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "501": {
            "$ref": "#/components/responses/NotImplemented"
          },
          "503": {
            "description": "`console_unavailable`: the key could not be verified; retry shortly.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/compute/v1/internal/jobs/{id}/artifacts/uploads": {
      "post": {
        "operationId": "presignArtifactUpload",
        "tags": [
          "internal"
        ],
        "summary": "Upload URL for one output file (job run step only)",
        "description": "Called by the job's own run step, not by customers: one file's absolute `path`, `size` and hex `sha256` in, a presigned PUT out, valid for at most an hour, that S3 accepts only with `x-amz-checksum-sha256` set to the returned value. Answers `501 not_implemented` in the dark build and in a deployment without artifact storage.",
        "security": [
          {
            "uploadToken": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/JobID"
          }
        ],
        "responses": {
          "200": {
            "description": "`<base64 sha256> <url>\\n`, so bash can read it without a JSON parser.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_path`, `invalid_size`, `invalid_sha256`, `no_outputs` or `malformed_body`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "`invalid_upload_token`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "`job_terminal`: the job has ended and its token with it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "413": {
            "description": "`file_too_large`: over 5 GiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "501": {
            "$ref": "#/components/responses/NotImplemented"
          },
          "503": {
            "description": "`storage_unavailable`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": [
                  "path",
                  "size",
                  "sha256"
                ],
                "properties": {
                  "path": {
                    "type": "string"
                  },
                  "size": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 5368709120
                  },
                  "sha256": {
                    "type": "string",
                    "pattern": "^[0-9a-f]{64}$"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/compute/v1/internal/jobs/{id}/artifacts/complete": {
      "post": {
        "operationId": "completeArtifactUpload",
        "tags": [
          "internal"
        ],
        "summary": "Write the output manifest (job run step only)",
        "description": "Called by the job's own run step after its uploads: compute-api lists the job's prefix and writes `jobs/<job_id>/_manifest.json` from what S3 holds. Calling it again rewrites it. Answers `501 not_implemented` in the dark build and in a deployment without artifact storage.",
        "security": [
          {
            "uploadToken": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/JobID"
          }
        ],
        "responses": {
          "200": {
            "description": "The manifest was written.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "object",
                    "files",
                    "truncated"
                  ],
                  "properties": {
                    "object": {
                      "type": "string",
                      "const": "compute.artifact_manifest"
                    },
                    "files": {
                      "type": "integer",
                      "minimum": 0
                    },
                    "truncated": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "`invalid_upload_token`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "`job_terminal`: the job has ended and its token with it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "501": {
            "$ref": "#/components/responses/NotImplemented"
          },
          "503": {
            "description": "`storage_unavailable`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "sk-... or uck_...",
        "description": "With `ACCOUNTS_BACKEND=console` (and in the dark build): the customer's existing Unify API key, the same one that calls api.unifyapi.ai/v1. A key not shaped like one (prefix `sk-`, at least 8 characters) is `401 missing_key`, and a Unify Compute `uck_` key is `401 invalid_key` without a console call. Otherwise it is verified through the console bridge and the answer is cached for at most 60 seconds, so a revoked key stops working within a minute: unknown, disabled or expired is `401 invalid_key`, a disabled account or suspended organisation `403`, and a console that cannot answer `503 console_unavailable`. The dark build checks only the shape.\n\nWith `ACCOUNTS_BACKEND=local`: a Unify Compute API key, `uck_` followed by 40 characters from `[0-9A-Za-z]`, created on the dashboard. Any other shape, `sk-` keys included, is `401 missing_key` without a database read. The key is looked up on every request, with no cache: unknown, revoked or expired is `401 invalid_key` on the very next request. A key's scopes decide what it may do: `read` only GETs; `jobs` also submits and cancels, and a `read`-only key on either is `403 permission_error`/`scope_denied`. The owner of everything the key reaches is `account:<id>`, and the per-key rate limits count per key."
      },
      "uploadToken": {
        "type": "http",
        "scheme": "bearer",
        "description": "A job's own upload token, from `UNIFY_COMPUTE_ARTIFACTS_TOKEN` in its environment. Valid for that job only, until launch + `max_runtime_seconds` + 2 h, and only while the job is not terminal."
      }
    },
    "parameters": {
      "JobID": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Job id, `job_` followed by a 26-character ULID.",
        "schema": {
          "type": "string"
        }
      },
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "Retained 24 hours. Same key and same body returns the original job with 200; same key and a different body is 409 `idempotency_conflict`; longer than 255 characters is 400 `invalid_idempotency_key`. Keys are per owner. The dark build ignores it.",
        "schema": {
          "type": "string",
          "minLength": 1,
          "maxLength": 255
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing or malformed key (`missing_key`), or a key that is not accepted: unknown, disabled, revoked or expired (`invalid_key`). `error.type` is `authentication_error`.",
        "headers": {
          "WWW-Authenticate": {
            "schema": {
              "type": "string"
            },
            "description": "`Bearer realm=\"compute\"`"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "error": {
                "type": "authentication_error",
                "code": "missing_key",
                "message": "Provide your Unify API key as 'Authorization: Bearer sk-...'."
              }
            }
          }
        }
      },
      "NotImplemented": {
        "description": "The dark build: this deployment has no job store, so the route exists and the request passed the key's shape check (and, for submit, validation) but nothing is run.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "error": {
                "type": "not_implemented",
                "code": "not_implemented",
                "message": "job lookup is not available yet; see docs/spec/job-api.md for the contract."
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "Job id unknown for the calling key's owner (`job_not_found`); another owner's job is indistinguishable from a missing one.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Per-key rate exceeded: 60 submits per hour, 600 reads per minute (`GET` on the job routes), as token buckets enforced per process. `error.type` and `error.code` are `rate_limited`. Cancel is not limited yet. Retry after the number of seconds in `Retry-After`.",
        "headers": {
          "Retry-After": {
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 1
            },
            "description": "Whole seconds to wait, at least 1."
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "error": {
                "type": "rate_limited",
                "code": "rate_limited",
                "message": "Per-key rate limit reached; retry after 60 s. See docs/spec/job-api.md, Rate limits."
              }
            }
          }
        }
      },
      "QuotaExceeded": {
        "description": "The submit would exceed an account quota (docs/spec/quotas.md). Refused whole, never a partial grant; `error.quotas` lists each limit exceeded with its headroom. No `Retry-After`: when a slot frees depends on other jobs' runtimes. Checked last, after every request error, on the resolved SKU (a template's default counts). Enforced by every build with a job store; the dark build does not check quotas.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "error": {
                "type": "quota_exceeded",
                "code": "quota_exceeded",
                "param": "sku",
                "message": "sku \"a10g-24gb-x1\" would exceed this account's quota (active jobs: 2 of 2 in use, this job needs 1, headroom 0; hyperscaler GPUs: 2 of 2 in use, this job needs 1, headroom 0). Nothing was submitted or held; wait for or cancel an active job, choose a smaller SKU, or ask support to raise the limit.",
                "quotas": [
                  {
                    "limit": "max_active_jobs",
                    "max": 2,
                    "used": 2,
                    "requested": 1,
                    "headroom": 0
                  },
                  {
                    "limit": "max_gpus",
                    "tier": "hyperscaler",
                    "max": 2,
                    "used": 2,
                    "requested": 1,
                    "headroom": 0
                  }
                ]
              }
            }
          }
        }
      },
      "AccountRefused": {
        "description": "With the console bridge, the key is valid but its account may not spend: `authentication_error`/`account_disabled` (the user is disabled) or `authentication_error`/`account_suspended` (the organisation is suspended). With `ACCOUNTS_BACKEND=local`, on a non-GET route: `permission_error`/`scope_denied`, the key has the `read` scope only.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      },
      "ConsoleUnavailable": {
        "description": "`console_unavailable`: the key could not be verified because the Unify API console did not answer, or the bridge is not configured in this deployment. Retry shortly.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      }
    },
    "schemas": {
      "ErrorEnvelope": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "$ref": "#/components/schemas/Error"
          }
        },
        "description": "OpenAI-style envelope so existing client error handling carries over."
      },
      "Error": {
        "type": "object",
        "required": [
          "type",
          "code",
          "message"
        ],
        "x-error-codes": {
          "invalid_request": [
            "malformed_body",
            "missing_sku",
            "unknown_sku",
            "sku_unavailable",
            "missing_max_runtime",
            "runtime_over_ceiling",
            "provider_not_offered",
            "unknown_template",
            "template_unavailable",
            "field_not_overridable",
            "missing_input",
            "missing_image",
            "missing_command",
            "invalid_env",
            "reserved_env",
            "invalid_input_source",
            "invalid_mount",
            "invalid_metadata",
            "invalid_idempotency_key",
            "invalid_status",
            "invalid_limit",
            "invalid_cursor"
          ],
          "authentication_error": [
            "missing_key",
            "invalid_key",
            "account_disabled",
            "account_suspended"
          ],
          "permission_error": [
            "scope_denied",
            "compute_not_enabled"
          ],
          "insufficient_balance": [
            "insufficient_balance"
          ],
          "quota_exceeded": [
            "quota_exceeded"
          ],
          "not_found": [
            "job_not_found"
          ],
          "conflict": [
            "idempotency_conflict",
            "job_terminal"
          ],
          "rate_limited": [],
          "internal_error": [
            "placement_failed"
          ],
          "not_implemented": [
            "not_implemented",
            "outputs_unsupported"
          ],
          "console_unavailable": [
            "console_unavailable"
          ],
          "upstream_unavailable": [
            "upstream_unavailable",
            "logs_unavailable"
          ],
          "storage_unavailable": []
        },
        "description": "`type` is the class (a closed set); `code` is the specific reason. `x-error-codes` lists, per type, the codes the service emits. A build without a job store emits a subset of them. Clients must tolerate codes not listed.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "invalid_request",
              "authentication_error",
              "permission_error",
              "insufficient_balance",
              "quota_exceeded",
              "not_found",
              "conflict",
              "rate_limited",
              "internal_error",
              "not_implemented",
              "console_unavailable",
              "upstream_unavailable",
              "storage_unavailable"
            ]
          },
          "code": {
            "type": "string"
          },
          "message": {
            "type": "string",
            "description": "Human-readable; not stable, do not parse."
          },
          "param": {
            "type": "string",
            "description": "The request field at fault, as a dotted path (e.g. `spec.max_runtime_seconds`). Omitted when not applicable."
          },
          "quotas": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/QuotaViolation"
            },
            "description": "Present only on `quota_exceeded`: every limit the submit would exceed, with usage and headroom, so a client never has to parse `message`."
          }
        }
      },
      "Health": {
        "type": "object",
        "required": [
          "status",
          "service",
          "build"
        ],
        "properties": {
          "status": {
            "type": "string",
            "const": "ok"
          },
          "service": {
            "type": "string",
            "const": "compute-api"
          },
          "build": {
            "type": "string",
            "description": "Git commit of the running binary; `dev` when unset."
          }
        }
      },
      "Tier": {
        "type": "string",
        "enum": [
          "hyperscaler",
          "neocloud",
          "marketplace"
        ],
        "description": "Trust level of every provider behind a SKU (ADR-0006). A job is never placed outside its SKU's tier."
      },
      "Provider": {
        "type": "object",
        "required": [
          "cloud"
        ],
        "description": "One place a SKU can run. Order within a SKU is preference order; SkyPilot fails over along it.",
        "properties": {
          "cloud": {
            "type": "string",
            "description": "SkyPilot cloud name, e.g. `aws`, `gcp`, `lambda`, `kubernetes`, `ssh`."
          },
          "instance_type": {
            "type": "string",
            "description": "Present for VM clouds."
          },
          "context": {
            "type": "string",
            "description": "Present for `kubernetes` and `ssh`: the kubeconfig context or node pool."
          },
          "regions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "SKU": {
        "type": "object",
        "required": [
          "id",
          "object",
          "gpu",
          "accelerator",
          "gpu_count",
          "gpu_memory_gb",
          "vcpus",
          "memory_gb",
          "price_usd_per_hour",
          "tier",
          "interruptible",
          "regions",
          "providers",
          "available"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "object": {
            "type": "string",
            "const": "compute.sku"
          },
          "gpu": {
            "type": "string",
            "description": "Display name, e.g. `NVIDIA A10G`."
          },
          "accelerator": {
            "type": "string",
            "description": "SkyPilot accelerator name, e.g. `A10G`, `A100-80GB`. May be empty for a SKU that is not `available`."
          },
          "gpu_count": {
            "type": "integer"
          },
          "gpu_memory_gb": {
            "type": "integer"
          },
          "vcpus": {
            "type": "integer"
          },
          "memory_gb": {
            "type": "integer"
          },
          "price_usd_per_hour": {
            "type": "string",
            "pattern": "^[0-9]+\\.[0-9]+$",
            "description": "Per hour for display; metered per second (`price_usd_per_hour / 3600`, rounded half-up to 6 decimals at settlement)."
          },
          "tier": {
            "description": "Empty string only on a SKU that is not `available`.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/Tier"
              },
              {
                "type": "string",
                "const": ""
              }
            ]
          },
          "interconnect": {
            "type": "string",
            "enum": [
              "pcie",
              "nvlink"
            ],
            "description": "How the SKU's GPUs are wired to each other. Required on an `available` SKU with `gpu_count` > 1; absent on single-GPU SKUs, where it has no meaning. Like `tier`, it is a promise every provider behind the SKU keeps."
          },
          "network": {
            "type": "string",
            "enum": [
              "ethernet",
              "infiniband"
            ],
            "description": "Node-to-node fabric. Informational: multi-node jobs are not sold, and nothing in the service reads this yet."
          },
          "interruptible": {
            "type": "boolean",
            "description": "Runs on spot capacity and may end `interrupted`."
          },
          "regions": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            },
            "description": "Union of the providers' regions."
          },
          "providers": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/Provider"
            }
          },
          "available": {
            "type": "boolean",
            "description": "Listed but not sold when false; submitting it is `400 sku_unavailable`."
          }
        }
      },
      "SKUList": {
        "type": "object",
        "required": [
          "object",
          "data"
        ],
        "properties": {
          "object": {
            "type": "string",
            "const": "list"
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SKU"
            }
          },
          "note": {
            "type": "string",
            "description": "Free-text caveat about the catalogue, e.g. that prices are placeholders. Empty when there is none."
          }
        }
      },
      "Upstream": {
        "type": "object",
        "required": [
          "object",
          "cloud",
          "enabled",
          "skus"
        ],
        "properties": {
          "object": {
            "type": "string",
            "const": "compute.upstream"
          },
          "cloud": {
            "type": "string"
          },
          "enabled": {
            "type": "boolean",
            "description": "Whether this deployment holds credentials for the cloud (`UPSTREAMS`)."
          },
          "skus": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "SKU ids that list this cloud as a provider."
          }
        }
      },
      "UpstreamList": {
        "type": "object",
        "required": [
          "object",
          "data"
        ],
        "properties": {
          "object": {
            "type": "string",
            "const": "list"
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Upstream"
            }
          }
        }
      },
      "JobStatus": {
        "type": "string",
        "enum": [
          "queued",
          "provisioning",
          "running",
          "succeeded",
          "failed",
          "interrupted",
          "cancelled"
        ],
        "description": "`succeeded`, `failed`, `interrupted`, `cancelled` are terminal. Allowed transitions: queued -> provisioning|cancelled; provisioning -> running|failed|interrupted|cancelled; running -> succeeded|failed|interrupted|cancelled."
      },
      "JobInput": {
        "type": "object",
        "required": [
          "source",
          "mount"
        ],
        "properties": {
          "source": {
            "type": "string",
            "description": "An `https://` URL, public or presigned by you (ADR-0005). Other sources, such as `s3://`, are refused by the backend when the job is launched; submit does not check them yet."
          },
          "mount": {
            "type": "string",
            "description": "Absolute directory. The run step downloads `source` into it, named after the URL's last path segment, before the command runs."
          }
        }
      },
      "JobOutput": {
        "type": "object",
        "required": [
          "mount"
        ],
        "properties": {
          "mount": {
            "type": "string",
            "description": "Absolute directory (not `/`); every regular file under it is uploaded when the command exits, whatever its status. At most 5 GiB per file and 10 000 files per job."
          }
        }
      },
      "JobSpec": {
        "type": "object",
        "required": [],
        "properties": {
          "image": {
            "type": "string",
            "description": "Container image, pulled on the node. Required (`missing_image`), unless `template` supplies it."
          },
          "command": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Full argv run in the image; the image's entrypoint is not used. Required (`missing_command`), unless `template` supplies it."
          },
          "env": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "inputs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/JobInput"
            },
            "description": "Each `source` is an `https://` URL ending in a file name, public or presigned; it is downloaded into `mount` before the command runs. Other sources are refused (`invalid_input_source`)."
          },
          "outputs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/JobOutput"
            }
          },
          "max_runtime_seconds": {
            "type": "integer",
            "minimum": 1,
            "maximum": 86400,
            "description": "The contract never allows more than 24 hours; a deployment's ceiling may be lower and is 4 hours (14400) on every deployment today. Above it is `400 runtime_over_ceiling`. Required unless `template` supplies it."
          }
        }
      },
      "SubmitJobRequest": {
        "type": "object",
        "required": [],
        "properties": {
          "template": {
            "type": "string",
            "description": "An `id` from GET /compute/v1/templates. The template supplies the spec and the default `sku`; the body may then send only the fields the template lists in `overridable`, and must bind a `source` to each of its input mounts. See docs/spec/templates.md."
          },
          "sku": {
            "type": "string",
            "description": "An `id` from GET /compute/v1/skus. Required unless `template` is set; with a template, allowed only if the template lists `sku` as overridable."
          },
          "providers": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Restrict placement to a subset of the SKU's provider clouds. Default: all of them."
          },
          "spec": {
            "$ref": "#/components/schemas/JobSpec"
          },
          "metadata": {
            "type": "object",
            "maxProperties": 16,
            "additionalProperties": {
              "type": "string"
            },
            "description": "At most 16 string pairs (`invalid_metadata`). Returned on the Job."
          }
        },
        "description": "Without `template`, `sku` and `spec.max_runtime_seconds` are required. With `template`, both default from it."
      },
      "JobUsage": {
        "type": "object",
        "required": [
          "gpu_seconds",
          "estimated_cost_usd",
          "settled_cost_usd",
          "hold_usd"
        ],
        "properties": {
          "gpu_seconds": {
            "type": "integer"
          },
          "estimated_cost_usd": {
            "type": "string",
            "pattern": "^[0-9]+\\.[0-9]+$",
            "description": "USD as a decimal string, never a float."
          },
          "settled_cost_usd": {
            "type": "string",
            "pattern": "^[0-9]+\\.[0-9]+$",
            "description": "USD as a decimal string, never a float."
          },
          "hold_usd": {
            "type": "string",
            "pattern": "^[0-9]+\\.[0-9]+$",
            "description": "USD as a decimal string, never a float."
          }
        }
      },
      "JobPlacement": {
        "type": "object",
        "required": [
          "cloud",
          "region",
          "instance_type"
        ],
        "properties": {
          "cloud": {
            "type": "string"
          },
          "region": {
            "type": "string"
          },
          "instance_type": {
            "type": "string"
          }
        }
      },
      "JobError": {
        "type": "object",
        "required": [
          "code",
          "message"
        ],
        "properties": {
          "code": {
            "type": "string",
            "description": "Why the job ended, e.g. `balance_exhausted`, `billing_unavailable`."
          },
          "message": {
            "type": "string"
          }
        }
      },
      "Job": {
        "type": "object",
        "required": [
          "id",
          "object",
          "status",
          "sku",
          "created_at",
          "started_at",
          "finished_at",
          "spec",
          "usage",
          "error",
          "placement",
          "metadata"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$"
          },
          "object": {
            "type": "string",
            "const": "compute.job"
          },
          "status": {
            "$ref": "#/components/schemas/JobStatus"
          },
          "sku": {
            "type": "string"
          },
          "created_at": {
            "type": "integer",
            "description": "Unix seconds."
          },
          "started_at": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Unix seconds."
          },
          "finished_at": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Unix seconds."
          },
          "spec": {
            "$ref": "#/components/schemas/JobSpec"
          },
          "usage": {
            "$ref": "#/components/schemas/JobUsage"
          },
          "error": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/JobError"
              },
              {
                "type": "null"
              }
            ]
          },
          "placement": {
            "description": "Where the job landed (upstreams.md R10): written when SkyPilot first reports the cluster UP, before the job reads `running`. Null until then, and stays null for a job that never ran.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/JobPlacement"
              },
              {
                "type": "null"
              }
            ]
          },
          "metadata": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          }
        }
      },
      "JobList": {
        "type": "object",
        "required": [
          "object",
          "data",
          "has_more"
        ],
        "properties": {
          "object": {
            "type": "string",
            "const": "list"
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Job"
            }
          },
          "has_more": {
            "type": "boolean"
          },
          "next": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pass as `after` to fetch the next page."
          }
        }
      },
      "ArtifactList": {
        "type": "object",
        "required": [
          "object",
          "data",
          "complete",
          "truncated"
        ],
        "properties": {
          "object": {
            "type": "string",
            "const": "list"
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Artifact"
            }
          },
          "complete": {
            "type": "boolean",
            "description": "False until the job's upload has finished and written its manifest: while the job runs, after a job that ended without reaching its upload (cancelled, preempted, stopped at `max_runtime_seconds`), and after retention deleted the files. `data` is then empty."
          },
          "truncated": {
            "type": "boolean",
            "description": "The job wrote more than 10 000 files; only the first 10 000 by path are listed."
          }
        }
      },
      "Event": {
        "type": "object",
        "required": [
          "id",
          "object",
          "job_id",
          "type",
          "created_at",
          "data"
        ],
        "description": "One entry of a job's append-only event log. Also the body of a webhook delivery.",
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^evt_[0-9A-HJKMNP-TV-Z]{26}$",
            "description": "`evt_` + ULID. The deduplication key: a webhook delivered twice carries the same id."
          },
          "object": {
            "type": "string",
            "const": "compute.event"
          },
          "job_id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "description": "One of `job.queued`, `job.provisioning`, `job.placed`, `job.running`, `job.failover`, `job.preempted`, `job.succeeded`, `job.failed`, `job.cancelled`, `billing.hold_extended`, `billing.balance_exhausted`. Clients must tolerate types not listed here."
          },
          "created_at": {
            "type": "integer",
            "description": "Unix seconds. Several events can share a second; list order is append order."
          },
          "data": {
            "type": "object",
            "description": "Type-specific payload; `{}` when the type carries nothing. See docs/spec/events.md."
          }
        }
      },
      "EventList": {
        "type": "object",
        "required": [
          "object",
          "data",
          "has_more"
        ],
        "properties": {
          "object": {
            "type": "string",
            "const": "list"
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Event"
            }
          },
          "has_more": {
            "type": "boolean"
          },
          "next": {
            "type": "string",
            "description": "Id of the last event returned, present whenever `data` is non-empty, so a poller can always resume from it."
          }
        }
      },
      "TemplateMount": {
        "type": "object",
        "required": [
          "mount",
          "description"
        ],
        "properties": {
          "mount": {
            "type": "string",
            "description": "Absolute path inside the job's container."
          },
          "description": {
            "type": "string"
          }
        }
      },
      "Template": {
        "type": "object",
        "required": [
          "id",
          "object",
          "title",
          "description",
          "image",
          "command",
          "env",
          "sku",
          "max_runtime_seconds",
          "inputs",
          "outputs",
          "overridable",
          "available"
        ],
        "description": "A named, reviewed job spec a submit can name in `template`.",
        "properties": {
          "id": {
            "type": "string",
            "description": "Lowercase slug; what a submit names."
          },
          "object": {
            "type": "string",
            "const": "compute.template"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "image": {
            "type": "string",
            "description": "Container image pinned by `@sha256` digest. Empty only on an unavailable placeholder."
          },
          "image_licence": {
            "type": "string"
          },
          "image_source": {
            "type": "string",
            "description": "Where the image and its licence were checked."
          },
          "command": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            },
            "description": "Full argv; the image's entrypoint is not used."
          },
          "env": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Defaults. A submit that may override `spec.env` merges over them key by key."
          },
          "sku": {
            "type": "string",
            "description": "Default SKU, an `id` from GET /compute/v1/skus."
          },
          "max_runtime_seconds": {
            "type": "integer",
            "minimum": 1,
            "maximum": 86400
          },
          "inputs": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/TemplateMount"
            },
            "description": "Mounts a submit must bind a `source` to, one each."
          },
          "outputs": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/TemplateMount"
            }
          },
          "overridable": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string",
              "enum": [
                "sku",
                "providers",
                "spec.command",
                "spec.env",
                "spec.max_runtime_seconds"
              ]
            },
            "description": "Request fields a submit naming this template may send."
          },
          "available": {
            "type": "boolean",
            "description": "Declared available and its default SKU is sold."
          },
          "unavailable_reason": {
            "type": "string",
            "description": "Present when `available` is false."
          }
        }
      },
      "TemplateList": {
        "type": "object",
        "required": [
          "object",
          "data"
        ],
        "properties": {
          "object": {
            "type": "string",
            "const": "list"
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Template"
            }
          }
        }
      },
      "QuotaViolation": {
        "type": "object",
        "required": [
          "limit",
          "max",
          "used",
          "requested",
          "headroom"
        ],
        "description": "One limit a submit would exceed (docs/spec/quotas.md): an account quota, or a `platform_` limit shared by every account, which counts all accounts' active jobs and has no `tier`.",
        "properties": {
          "limit": {
            "type": "string",
            "enum": [
              "max_active_jobs",
              "max_queued_jobs",
              "max_gpus",
              "platform_max_active_jobs",
              "platform_max_gpus"
            ]
          },
          "tier": {
            "type": "string",
            "enum": [
              "hyperscaler",
              "neocloud",
              "marketplace"
            ],
            "description": "Present for `max_gpus`, which is per provider tier."
          },
          "max": {
            "type": "integer",
            "description": "The account's limit."
          },
          "used": {
            "type": "integer",
            "description": "Already in use by the account's active or queued jobs."
          },
          "requested": {
            "type": "integer",
            "description": "What this submit would add."
          },
          "headroom": {
            "type": "integer",
            "description": "`max - used`, never negative: the most a submit could ask for now."
          }
        }
      },
      "Artifact": {
        "type": "object",
        "required": [
          "object",
          "path",
          "size",
          "sha256",
          "url",
          "expires_at"
        ],
        "description": "One output file (ADR-0005). Retained 30 days from upload.",
        "properties": {
          "object": {
            "type": "string",
            "const": "compute.artifact"
          },
          "path": {
            "type": "string",
            "description": "Where the job wrote the file, under one of `spec.outputs[].mount`."
          },
          "size": {
            "type": "integer",
            "minimum": 0
          },
          "sha256": {
            "type": "string",
            "description": "Hex SHA-256, verified by S3 when the file was uploaded."
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Presigned GET."
          },
          "expires_at": {
            "type": "integer",
            "description": "Unix seconds when `url` stops working: at most one hour from the call, sooner if the signing credentials expire first."
          }
        }
      }
    }
  }
}
