{
  "openapi": "3.1.0",
  "info": {
    "title": "Latchkey Jobs API",
    "version": "1.0.0",
    "summary": "Run a single command on a fresh Latchkey runner, with no GitHub in the path.",
    "description": "The Jobs API gives a coding agent or CI system direct access to an ephemeral Latchkey runner. A job runs ONE command on a fresh Ubuntu 24.04 x86_64 machine that is destroyed afterwards. Nothing registers with GitHub, so jobs keep running when GitHub Actions is unavailable.\n\nTypical lifecycle: create a job, optionally upload a context archive to the returned presigned URL, submit it, then poll status and stream logs until a terminal state.\n\nThe Model Context Protocol server is documented separately at https://latchkey.dev/.well-known/mcp/manifest.json.\n\nAuthentication is a Latchkey API key sent as a bearer token. Keys are created in the Latchkey dashboard and carry scopes: `jobs:read` for status, logs and listing; `jobs:run` for create, submit and cancel.",
    "contact": {
      "name": "Latchkey Support",
      "url": "https://latchkey.dev/support",
      "email": "support@latchkey.dev"
    },
    "license": {
      "name": "Proprietary",
      "url": "https://latchkey.dev/terms"
    }
  },
  "servers": [
    {
      "url": "https://api.latchkey.dev",
      "description": "Production"
    }
  ],
  "externalDocs": {
    "description": "Latchkey CLI and agent documentation",
    "url": "https://latchkey.dev/documentation/latchkey-cli"
  },
  "security": [
    {
      "latchkeyApiKey": []
    }
  ],
  "tags": [
    {
      "name": "Jobs",
      "description": "Create, submit, observe and cancel runner jobs."
    }
  ],
  "paths": {
    "/jobs": {
      "post": {
        "operationId": "createJob",
        "tags": ["Jobs"],
        "summary": "Create a job",
        "description": "Reserves a job in state `created` and returns its id. If `context_bytes` is supplied, the response includes a presigned URL to upload a gzipped tar archive of your working tree before submitting. Creating a job does not start it; call submitJob next.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateJobRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Job created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateJobResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "get": {
        "operationId": "listJobs",
        "tags": ["Jobs"],
        "summary": "List recent jobs",
        "description": "Returns recent jobs for the organization that owns the API key, newest first. Served from durable storage, so it outlives an individual job record and can answer what was run last week.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum jobs to return.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of jobs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobListResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/jobs/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/JobId"
        }
      ],
      "get": {
        "operationId": "getJob",
        "tags": ["Jobs"],
        "summary": "Get job status",
        "description": "Returns the current state of one job, including exit code and failure reason once terminal. A submitted job that has not started within one hour is reported as `expired`.",
        "responses": {
          "200": {
            "description": "Current job status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobStatusResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/jobs/{id}/submit": {
      "parameters": [
        {
          "$ref": "#/components/parameters/JobId"
        }
      ],
      "post": {
        "operationId": "submitJob",
        "tags": ["Jobs"],
        "summary": "Submit a job for execution",
        "description": "Moves a job from `created` to `queued` and enqueues a runner for it. Only a job in `created` may be submitted, so a failed or cancelled job can never be resubmitted. Upload the context archive first if you created the job with `context_bytes`.",
        "responses": {
          "200": {
            "description": "Job queued.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SubmitJobResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Job is not in a submittable state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/jobs/{id}/logs": {
      "parameters": [
        {
          "$ref": "#/components/parameters/JobId"
        }
      ],
      "get": {
        "operationId": "getJobLogs",
        "tags": ["Jobs"],
        "summary": "Read job logs",
        "description": "Returns ordered log chunks from the given cursor. Poll with the returned `next_cursor` until `complete` is true to stream a running job's output.",
        "parameters": [
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Chunk index to read from. Omit to start at the beginning.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Log chunks.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobLogsResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/jobs/{id}/cancel": {
      "parameters": [
        {
          "$ref": "#/components/parameters/JobId"
        }
      ],
      "post": {
        "operationId": "cancelJob",
        "tags": ["Jobs"],
        "summary": "Cancel a job",
        "description": "Requests cancellation. A job that has not started is cancelled immediately; a running job is signalled and terminalises shortly after. Cancellation is idempotent.",
        "responses": {
          "200": {
            "description": "Cancellation recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CancelJobResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "latchkeyApiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "A Latchkey API key, sent as `Authorization: Bearer lk_live_...`. Create one in the Latchkey dashboard. Scopes: `jobs:read` for getJob, getJobLogs and listJobs; `jobs:run` for createJob, submitJob and cancelJob."
      }
    },
    "parameters": {
      "JobId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Job id returned by createJob.",
        "schema": {
          "type": "string",
          "pattern": "^cli-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
          "examples": ["cli-3f2a1b4c-5d6e-7f80-91a2-b3c4d5e6f708"]
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "The request was malformed or exceeded a documented limit.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing or invalid API key.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "The key is valid but lacks the required scope, or the organization is not entitled.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "No such job for this organization.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Per-organization job creation quota exceeded.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "RunnerSize": {
        "type": "string",
        "description": "Latchkey runner t-shirt size. Determines vCPU and memory.",
        "enum": ["small", "medium", "large", "xlarge"]
      },
      "JobState": {
        "type": "string",
        "description": "Job lifecycle state. `succeeded`, `failed`, `cancelled` and `expired` are terminal.",
        "enum": [
          "created",
          "queued",
          "provisioning",
          "running",
          "succeeded",
          "failed",
          "cancelled",
          "expired"
        ]
      },
      "CreateJobRequest": {
        "type": "object",
        "required": ["command", "runner_size"],
        "properties": {
          "command": {
            "type": "string",
            "maxLength": 16384,
            "description": "The single shell command to run on the runner.",
            "examples": ["npm ci && npm test"]
          },
          "runner_size": {
            "$ref": "#/components/schemas/RunnerSize"
          },
          "env": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Environment variables for the command. At most 64 keys and 32768 bytes total.",
            "maxProperties": 64
          },
          "timeout_seconds": {
            "type": "integer",
            "minimum": 30,
            "maximum": 7200,
            "default": 1800,
            "description": "Wall-clock limit for the command."
          },
          "context_bytes": {
            "type": "integer",
            "minimum": 1,
            "maximum": 209715200,
            "description": "Byte length of the gzipped tar archive you intend to upload. Supplying it returns a presigned upload URL bound to this exact length."
          }
        }
      },
      "CreateJobResponse": {
        "type": "object",
        "required": ["job_id"],
        "properties": {
          "job_id": {
            "type": "string",
            "description": "Identifier for the new job."
          },
          "context_upload_url": {
            "type": "string",
            "format": "uri",
            "description": "Presigned PUT URL for the context archive. Present only when `context_bytes` was supplied. Upload with Content-Type `application/gzip` before calling submitJob."
          }
        }
      },
      "SubmitJobResponse": {
        "type": "object",
        "required": ["job_id", "state"],
        "properties": {
          "job_id": {
            "type": "string",
            "description": "Identifier of the submitted job."
          },
          "state": {
            "$ref": "#/components/schemas/JobState"
          },
          "context_present": {
            "type": ["boolean", "null"],
            "description": "Whether a context archive was found for this job."
          }
        }
      },
      "JobStatusResponse": {
        "type": "object",
        "required": ["job_id", "state", "runner_size", "timeout_seconds", "created_at"],
        "properties": {
          "job_id": {
            "type": "string",
            "description": "Identifier of the job."
          },
          "state": {
            "$ref": "#/components/schemas/JobState"
          },
          "runner_size": {
            "$ref": "#/components/schemas/RunnerSize"
          },
          "timeout_seconds": {
            "type": "integer",
            "description": "Wall-clock limit in seconds."
          },
          "cancel_requested": {
            "type": "boolean",
            "description": "Whether cancellation has been requested."
          },
          "exit_code": {
            "type": ["integer", "null"],
            "description": "Process exit code once the command has finished."
          },
          "failure_reason": {
            "type": ["string", "null"],
            "description": "Why the job failed, when it did."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "submitted_at": {
            "type": ["string", "null"],
            "format": "date-time"
          },
          "started_at": {
            "type": ["string", "null"],
            "format": "date-time"
          },
          "completed_at": {
            "type": ["string", "null"],
            "format": "date-time"
          }
        }
      },
      "JobListItem": {
        "type": "object",
        "required": ["job_id", "conclusion", "runner_size", "command_label", "queued_at"],
        "properties": {
          "job_id": {
            "type": "string",
            "description": "Identifier of the job."
          },
          "conclusion": {
            "type": "string",
            "description": "Terminal outcome, or an empty string while the job is still in flight."
          },
          "runner_size": {
            "type": "string",
            "description": "Runner size the job ran on."
          },
          "command_label": {
            "type": "string",
            "description": "Binary plus first subcommand, with arguments scrubbed."
          },
          "exit_code": {
            "type": ["integer", "null"],
            "description": "Process exit code."
          },
          "duration_ms": {
            "type": "integer",
            "description": "Run duration in milliseconds."
          },
          "queued_at": {
            "type": "string",
            "format": "date-time"
          },
          "completed_at": {
            "type": ["string", "null"],
            "format": "date-time"
          }
        }
      },
      "JobListResponse": {
        "type": "object",
        "required": ["jobs"],
        "properties": {
          "jobs": {
            "type": "array",
            "description": "Jobs, newest first.",
            "items": {
              "$ref": "#/components/schemas/JobListItem"
            }
          }
        }
      },
      "JobLogChunk": {
        "type": "object",
        "required": ["index", "content"],
        "properties": {
          "index": {
            "type": "integer",
            "description": "Zero-based chunk index."
          },
          "content": {
            "type": "string",
            "description": "Raw log text for this chunk."
          }
        }
      },
      "JobLogsResponse": {
        "type": "object",
        "required": ["chunks", "next_cursor", "complete"],
        "properties": {
          "chunks": {
            "type": "array",
            "description": "Ordered log chunks from the requested cursor.",
            "items": {
              "$ref": "#/components/schemas/JobLogChunk"
            }
          },
          "next_cursor": {
            "type": "integer",
            "description": "Cursor to pass on the next poll."
          },
          "complete": {
            "type": "boolean",
            "description": "True once the job is terminal and all logs have been returned."
          }
        }
      },
      "CancelJobResponse": {
        "type": "object",
        "required": ["job_id", "state", "cancel_requested"],
        "properties": {
          "job_id": {
            "type": "string",
            "description": "Identifier of the job."
          },
          "state": {
            "$ref": "#/components/schemas/JobState"
          },
          "cancel_requested": {
            "type": "boolean",
            "description": "Always true once cancellation has been recorded."
          }
        }
      },
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable description of the problem."
          }
        }
      }
    }
  }
}
