{"openapi":"3.1.0","info":{"title":"SOPHON Encoding API","version":"1.0.0","contact":{"name":"Liqhtworks","url":"https://sophon.rs"},"description":"REST API for submitting, monitoring, and retrieving SOPHON encoding jobs.\n\n## Client Libraries\n\nFor server-side application integrations, start with one of the official\nSOPHON client libraries. Current published version: `0.1.5`.\n\n| Language | Package | Install |\n|---|---|---|\n| TypeScript / JavaScript | [`@liqhtworks/sophon-sdk`](https://www.npmjs.com/package/@liqhtworks/sophon-sdk) | `npm install @liqhtworks/sophon-sdk` |\n| Python | [`sophon-sdk`](https://pypi.org/project/sophon-sdk/) | `pip install sophon-sdk` |\n| Go | [`github.com/Liqhtworks/sophon-sdk-go`](https://github.com/Liqhtworks/sophon-sdk-go) | `go get github.com/Liqhtworks/sophon-sdk-go@latest` |\n\nThese libraries are generated from this OpenAPI document and include the\ngenerated endpoint clients plus helpers for chunked uploads, terminal job\npolling, upload-backed job source construction, and webhook signature\nverification.\n\nWe do not currently publish official SDKs for Ruby, Java, Kotlin, Swift,\nPHP, .NET, or Rust. Use the REST API directly from those languages.\n\nAuthentication is via Bearer API key or session cookie. Create API keys in\nthe SOPHON dashboard at https://sophon.rs/account/general and pass\nthem as `Authorization: Bearer xt_live_...`. All POST endpoints require an\n`Idempotency-Key` header. List endpoints use opaque cursor-based pagination.\n\nSee **SDKs** for install commands and quickstarts. See **Integration\nexample** for a production state-machine pattern and raw HTTP adapter\nguidance.\n"},"servers":[{"url":"https://api.liqhtworks.xyz","description":"Production"}],"tags":[{"name":"Jobs","description":"Submit, inspect, cancel, and retrieve encoding jobs."},{"name":"Uploads","description":"Create chunked uploads and assemble source media."},{"name":"Webhooks","description":"Manage outbound terminal-job webhook endpoints."},{"name":"Webhook Events","description":"Outbound webhook payloads delivered by SOPHON when a job reaches a\nterminal state.\n\n### Delivery semantics\n\n- **Trigger**: a delivery is enqueued when a job transitions to\n  `completed`, `failed`, or `canceled`. No deliveries for\n  `queued`, `running`, or interim `progress` events.\n- **Subscription**: only the webhooks listed in\n  `CreateJobRequest.webhook_ids` for that specific job receive\n  deliveries. Registering a webhook does not subscribe it\n  retroactively or to all org jobs.\n- **Timeout**: each delivery attempt waits up to **10 seconds**\n  for a 2xx response. Anything beyond that, or any non-2xx, is\n  treated as a failure and triggers a retry.\n- **Retries**: up to **3 total attempts** (1 initial + 2 retries).\n  Backoff between attempts is approximately **10s, then 60s**\n  (the third gap, if it were used, would be 300s — but the 3rd\n  attempt is the last). Deliveries that exhaust the retry budget\n  are logged and dropped; there is no dead-letter queue the\n  customer can inspect.\n- **Ordering**: not guaranteed. A consumer that needs strict\n  ordering should treat `X-Turbo-Event-Id` as the dedupe key and\n  reconcile state from `GET /v1/jobs/{id}` if events arrive out\n  of order.\n- **At-least-once**: a delivery may be retried after a consumer's\n  transient failure, so the same `event_id` can arrive more than\n  once. Idempotent processing is required.\n- **Signature**: every delivery carries `X-Turbo-Signature-256`\n  (HMAC-SHA256 of `\"{X-Turbo-Timestamp}.{raw_body}\"` using the\n  per-webhook secret). Verify on the raw bytes BEFORE JSON-parsing.\n"},{"name":"Downloads","description":"Resolve signed output download tokens."},{"name":"Health","description":"Service health, readiness, version, and metrics endpoints."},{"name":"SDKs","description":"Official SDKs are the recommended way to integrate SOPHON from\nserver-side application code. Current published version: `0.1.5`.\n\nRepositories and package registries:\n\n| Language | Install | Links |\n|---|---|---|\n| TypeScript / JavaScript | `npm install @liqhtworks/sophon-sdk` | [npm](https://www.npmjs.com/package/@liqhtworks/sophon-sdk), [GitHub](https://github.com/Liqhtworks/sophon-sdk-typescript) |\n| Python | `pip install sophon-sdk` | [PyPI](https://pypi.org/project/sophon-sdk/), [GitHub](https://github.com/Liqhtworks/sophon-sdk-python) |\n| Go | `go get github.com/Liqhtworks/sophon-sdk-go@latest` | [GitHub](https://github.com/Liqhtworks/sophon-sdk-go) |\n\n### Get an API key\n\nCreate a server-side API key in the dashboard:\nhttps://sophon.rs/account/general\n\n```bash\nexport SOPHON_API_KEY=xt_live_...\nexport SOPHON_BASE_URL=https://api.liqhtworks.xyz\n```\n\nKeep API keys on the server. Do not ship them in browser bundles,\nmobile apps, public repositories, logs, or analytics events.\n\n### What the helpers cover\n\n| Helper | Languages | Purpose |\n|---|---|---|\n| `uploadFile` / `upload_file` / `UploadFile` | TypeScript, Python, Go | Chunked upload orchestration with bounded concurrency, retries, resume, and progress callbacks. |\n| `JobSource.upload` / `JobSource.Upload` | TypeScript, Python, Go | Construct a typed upload-backed job source from a completed upload ID. |\n| `waitForJob` / `wait_for_job` / `WaitForJob` | TypeScript, Python, Go | Poll until a terminal job status with timeout and typed terminal/timeout errors. |\n| `verifyWebhookSignature` / `verify_webhook_signature` / `VerifyWebhookSignature` | TypeScript, Python, Go | Verify `X-Turbo-Signature-256` over the raw request body before JSON parsing. |\n\n### TypeScript quickstart\n\nRequires Node 18+ or a runtime with `fetch`, `Blob`, `AbortController`,\nand Web Crypto.\n\n```ts\nimport { Blob } from \"node:buffer\";\nimport { randomUUID } from \"node:crypto\";\nimport { readFile, writeFile } from \"node:fs/promises\";\nimport {\n  Configuration,\n  JobProfile,\n  JobSource,\n  JobStatus,\n  JobsApi,\n  UploadsApi,\n  uploadFile,\n  waitForJob,\n} from \"@liqhtworks/sophon-sdk\";\n\nconst apiKey = process.env.SOPHON_API_KEY;\nif (!apiKey) throw new Error(\"SOPHON_API_KEY is required\");\n\nconst basePath = process.env.SOPHON_BASE_URL ?? \"https://api.liqhtworks.xyz\";\nconst config = new Configuration({ basePath, accessToken: apiKey });\nconst uploads = new UploadsApi(config);\nconst jobs = new JobsApi(config);\n\nconst bytes = await readFile(\"source.mov\");\nconst upload = await uploadFile({\n  api: uploads,\n  source: new Blob([bytes], { type: \"video/quicktime\" }),\n  fileName: \"source.mov\",\n  mimeType: \"video/quicktime\",\n  concurrency: 4,\n});\n\nconst job = await jobs.createJob({\n  idempotencyKey: randomUUID(),\n  createJobRequest: {\n    source: JobSource.upload(upload.uploadId),\n    profile: JobProfile.SOPHON_AUTO,\n  },\n});\n\nconst final = await waitForJob({\n  api: jobs,\n  jobId: job.id,\n  timeoutMs: 30 * 60 * 1000,\n});\nif (final.status !== JobStatus.COMPLETED) {\n  throw new Error(`job ended in ${final.status}`);\n}\n\nconst redirect = await fetch(`${basePath}/v1/jobs/${final.id}/output`, {\n  headers: { authorization: `Bearer ${apiKey}` },\n  redirect: \"manual\",\n});\nconst location = redirect.headers.get(\"location\");\nif (!location) throw new Error(\"missing output redirect\");\n\nconst download = await fetch(new URL(location, basePath));\nawait writeFile(\"sophon-output.mp4\", Buffer.from(await download.arrayBuffer()));\n```\n\n### Python quickstart\n\nRequires Python 3.9+.\n\n```python\nimport os\nimport urllib.parse\nimport urllib.request\nimport uuid\nfrom pathlib import Path\n\nimport sophon_sdk\nfrom sophon_sdk.models.create_job_request import CreateJobRequest\n\napi_key = os.environ[\"SOPHON_API_KEY\"]\nbase_url = os.getenv(\"SOPHON_BASE_URL\", \"https://api.liqhtworks.xyz\")\n\nclass NoRedirectHandler(urllib.request.HTTPRedirectHandler):\n    def http_error_302(self, req, fp, code, msg, headers):\n        response = urllib.request.addinfourl(fp, headers, req.get_full_url())\n        response.code = code\n        return response\n\n    http_error_301 = http_error_303 = http_error_307 = http_error_302\n\nconfiguration = sophon_sdk.Configuration(host=base_url, access_token=api_key)\n\nwith sophon_sdk.ApiClient(configuration) as api_client:\n    uploads = sophon_sdk.UploadsApi(api_client)\n    jobs = sophon_sdk.JobsApi(api_client)\n\n    upload = sophon_sdk.upload_file(\n        uploads,\n        Path(\"source.mov\"),\n        file_name=\"source.mov\",\n        mime_type=\"video/quicktime\",\n        concurrency=4,\n    )\n\n    job = sophon_sdk.create_job(\n        jobs,\n        idempotency_key=str(uuid.uuid4()),\n        create_job_request=CreateJobRequest(\n            source=sophon_sdk.JobSource.upload(upload.upload_id),\n            profile=\"sophon-auto\",\n        ),\n    )\n\n    final = sophon_sdk.wait_for_job(jobs, job.id, timeout_seconds=30 * 60)\n    if final.status != \"completed\":\n        raise RuntimeError(f\"job ended in {final.status}\")\n\n    req = urllib.request.Request(\n        f\"{base_url}/v1/jobs/{final.id}/output\",\n        headers={\"Authorization\": f\"Bearer {api_key}\"},\n    )\n    opener = urllib.request.build_opener(NoRedirectHandler())\n    redirect = opener.open(req)\n    location = redirect.headers.get(\"Location\")\n    if not location:\n        raise RuntimeError(\"missing output redirect\")\n\n    download_url = urllib.parse.urljoin(base_url.rstrip(\"/\") + \"/\", location)\n    with urllib.request.urlopen(download_url, timeout=60) as response:\n        Path(\"sophon-output.mp4\").write_bytes(response.read())\n```\n\n### Go quickstart\n\nRequires Go 1.22+.\n\n```go\npackage main\n\nimport (\n    \"context\"\n    \"os\"\n    \"time\"\n\n    sophon \"github.com/Liqhtworks/sophon-sdk-go\"\n    \"github.com/Liqhtworks/sophon-sdk-go/helpers\"\n    \"github.com/google/uuid\"\n)\n\nfunc main() {\n    ctx := context.Background()\n    apiKey := os.Getenv(\"SOPHON_API_KEY\")\n    baseURL := os.Getenv(\"SOPHON_BASE_URL\")\n    if baseURL == \"\" {\n        baseURL = \"https://api.liqhtworks.xyz\"\n    }\n\n    cfg := sophon.NewConfiguration()\n    cfg.Servers = sophon.ServerConfigurations{{URL: baseURL}}\n    cfg.AddDefaultHeader(\"Authorization\", \"Bearer \"+apiKey)\n    client := sophon.NewAPIClient(cfg)\n\n    uploads := helpers.NewUploadsClient(client.UploadsAPI)\n    jobs := helpers.NewJobsClient(client.JobsAPI)\n\n    reader, size, closeFn, err := helpers.OpenFileForUpload(\"source.mov\")\n    if err != nil {\n        panic(err)\n    }\n    defer closeFn()\n\n    upload, err := helpers.UploadFile(\n        ctx,\n        uploads,\n        reader,\n        size,\n        \"source.mov\",\n        \"video/quicktime\",\n        helpers.UploadFileOptions{Concurrency: 4},\n    )\n    if err != nil {\n        panic(err)\n    }\n\n    job, _, err := client.JobsAPI.CreateJob(ctx).\n        IdempotencyKey(uuid.NewString()).\n        CreateJobRequest(sophon.CreateJobRequest{\n            Source: helpers.JobSource.Upload(upload.UploadID),\n            Profile: sophon.SOPHON_AUTO,\n        }).\n        Execute()\n    if err != nil {\n        panic(err)\n    }\n\n    final, err := helpers.WaitForJob(ctx, jobs, job.GetId(), helpers.WaitForJobOptions{\n        Timeout: 30 * time.Minute,\n    })\n    if err != nil {\n        panic(err)\n    }\n    if final.Status != \"completed\" {\n        panic(\"job ended in \" + final.Status)\n    }\n}\n```\n\n### Profile choice\n\nUse `sophon-auto` for production unless you need deterministic encoder\nsettings. The SDK README quickstarts may use `sophon-espresso` for fast\nsmoke tests, but production apps should default to `sophon-auto`.\n\n### Webhooks\n\nVerify webhook signatures with the raw request body before JSON parsing.\nComplete server examples are available in each SDK repository:\n\n- TypeScript: Express\n- Python: FastAPI\n- Go: `net/http`\n"},{"name":"Integration example","description":"A real-world walkthrough of how [Daisy](https://daisy.so) wires SOPHON into\ntwo production flows — user-uploaded video compression and automatic\npost-generation encoding after video rendering. Both converge on the same\nadapter and state machine; only the source differs.\n\nThe official SDKs cover the common upload, create-job, polling, and\nwebhook-verification paths. The lower-level patterns below are useful\nwhen you need a custom HTTP adapter or a durable production state machine.\n\n### 1. One thin adapter, one method per endpoint\n\nKeep the HTTP surface boring. Axios (or your stack's equivalent), a\nper-endpoint idempotency key, and profile names that the server validates:\n\n```ts\n@Injectable()\nexport class SophonService {\n  private client() {\n    return axios.create({\n      baseURL: process.env.SOPHON_BASE_URL,\n      headers: { Authorization: `Bearer ${process.env.SOPHON_API_KEY}` },\n      timeout: 60_000,\n    });\n  }\n\n  async createUploadSession(req, idempotencyKey) { /* POST /v1/uploads */ }\n  async uploadChunk(uploadId, partNumber, bytes) { /* PUT /v1/uploads/{id}/parts/{n} */ }\n  async completeUpload(uploadId, idempotencyKey) { /* POST /v1/uploads/{id}/complete */ }\n  async createJob(req, idempotencyKey) { /* POST /v1/jobs */ }\n  async getJob(id) { /* GET /v1/jobs/{id} */ }\n  async downloadOutputStream(jobId) { /* GET /v1/jobs/{id}/output */ }\n}\n```\n\n**Suffix idempotency keys per endpoint.** SOPHON scopes dedupe per route\nbut a shared key collides across retries that hit different endpoints.\nDo this:\n\n```ts\nconst base = `video:${video.id}:v1`;\nawait sophon.createUploadSession(req,  `${base}:create-upload`);\nawait sophon.completeUpload(uploadId,  `${base}:complete-upload`);\nawait sophon.createJob(req,            `${base}:create-job`);\n```\n\n**Profile names are strings, not an enum.** We add and rename profiles\n(`sophon-espresso` → `sophon-auto` → future variants). A TypeScript union\nwill drift; let the server validate.\n\n### 2. Model your pipeline as a state machine\n\nPersist a single `sophonState` JSON column per row. `jobId === null`\nroutes to dispatch; anything else polls that job:\n\n```ts\ninterface SophonState {\n  jobId: string | null;          // null = not dispatched; string = poll it\n  uploadId?: string;             // persist between upload + createJob\n  profile?: string;              // sophon-auto | sophon-espresso | ...\n  dispatchRetries: number;       // 3 strikes → fallback\n  downloadRetries: number;\n  lastError?: { stage, code, message, at };\n}\n\n// In your cron (5-second tick is plenty):\nif (state.jobId === null) {\n  await dispatch(video, state);  // upload + createJob\n} else {\n  await poll(video, state);      // getJob + (if completed) downloadAndComplete\n}\n```\n\nPersisting `uploadId` between the upload completion and the `createJob`\ncall matters — a crash in that window otherwise re-uploads the file.\n\n### 3. Stream for large sources; buffer for small\n\nUser-uploaded sources can be 1 GB+. Stream S3 → SOPHON in chunks equal\nto `session.chunk_size` from the createUploadSession response:\n\n```ts\nasync uploadStream(stream, fileName, mimeType, fileSize) {\n  const session = await this.createUploadSession({\n    file_name: fileName, file_size: fileSize, mime_type: mimeType,\n  });\n  let partIndex = 0, buffer = Buffer.alloc(0);\n  for await (const chunk of stream) {\n    buffer = Buffer.concat([buffer, chunk]);\n    while (buffer.length >= session.chunk_size) {\n      await this.uploadChunk(session.id, partIndex++,\n        buffer.subarray(0, session.chunk_size));\n      buffer = buffer.subarray(session.chunk_size);\n    }\n  }\n  if (buffer.length > 0) {\n    await this.uploadChunk(session.id, partIndex, buffer);\n  }\n  return this.completeUpload(session.id);\n}\n```\n\nGenerated outputs from a model run are typically <30 MB — for those, a\nbuffered upload path is simpler and avoids managing a stream lifetime.\n\n### 4. Always keep a fallback URL\n\nBefore a row enters your encoding state, make sure the source is already\nplayable from your CDN. Every SOPHON failure then degrades to \"use the\noriginal\" — the user's video never disappears because SOPHON is slow or\ndown. This is the single most important invariant:\n\n```ts\nawait videoRepository.update({ id: video.id }, {\n  videoUrl: sourceCloudfrontUrl,   // fallback URL, stays intact\n  status: VideoStatus.EncodingPending,\n  sophonState: { jobId: null, profile, dispatchRetries: 0, downloadRetries: 0 },\n  sourceFileSize: sourceBytes,\n});\n```\n\nOn any terminal failure (structured `retryable: false`, retry budget\nexhausted, 404 on getJob, 23h stuck-row guard), flip status back to\n`Done` with `videoUrl` unchanged. SOPHON is enhancement, not a delivery\ndependency.\n\n### 5. Handle the \"no-gain\" success path\n\n`sophon-auto` runs a pre-probe and, when it decides the output wouldn't\nbe smaller than the source, returns `final_artifact: \"original\"` and\n`saved_percent: 0`. Skip the output download — the source already lives\nin your bucket:\n\n```ts\nif (job.status === 'completed') {\n  if (job.final_artifact === 'original') {\n    // Persist outputFileSize = sourceFileSize so your UI shows\n    // \"no reduction\" instead of a missing value.\n    await completeWithFallbackOutput(video, job.output?.bytes ?? null);\n    return;\n  }\n  await downloadAndComplete(video, state, job.output?.bytes ?? null);\n}\n```\n\n### 6. Finalize by streaming into your own storage\n\n`GET /v1/jobs/{id}/output` returns a 302 to a presigned URL with a 24h\nTTL. Stream that directly into your bucket — no temp file, no buffering:\n\n```ts\nconst { stream } = await sophon.downloadOutputStream(state.jobId);\nconst outputKey = `encoded/${video.userId}/${video.id}.mp4`;\nawait fileService.uploadStream(outputKey, stream, 'video/mp4');\nawait videoRepository.update({ id: video.id }, {\n  videoUrl: fileService.cloudfrontUrl(outputKey),\n  outputFileSize: sophonOutputBytes,\n  status: VideoStatus.Done,\n});\n```\n\n### 7. Failure taxonomy\n\n| Error | Handling |\n|---|---|\n| Structured `retryable: false` from SOPHON | Terminal. Fall back to `Done` with source URL. |\n| Retryable upload / createJob failure | Increment `dispatchRetries`; after 3, fall back. |\n| Retryable download failure | Increment `downloadRetries`; after 3, fall back. |\n| `getJob` → HTTP 404 | Terminal. Job expired or never created. Fall back. |\n| Transient poll network error | Do nothing; next tick retries. Don't burn retry budget. |\n| Row stuck in encode state > 23h | Fall back (safety net against orphans). |\n\n### Minimal config\n\n```bash\nSOPHON_API_KEY=xt_live_...\nSOPHON_BASE_URL=https://api.liqhtworks.xyz\n```\n"}],"security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"components":{"securitySchemes":{"bearerApiKey":{"type":"http","scheme":"bearer","description":"API key passed as `Authorization: Bearer <key>`.\nKeys are scoped to an organization and carry permission scopes\n(e.g. jobs:create, uploads:write, webhooks:manage).\n"},"sessionCookie":{"type":"apiKey","in":"cookie","name":"sophon_api_session","description":"First-party dashboard session cookie (JWT, HS256)."}},"headers":{"XRequestId":{"description":"Unique ID for this request. Use in support tickets.","schema":{"type":"string"}}},"parameters":{"IdempotencyKey":{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","minLength":1,"maxLength":128},"description":"Client-generated UUID or string for exactly-once semantics.\nRequired on all POST endpoints. Replaying the same key with the\nsame request body returns the original response without side effects.\n"},"CursorParam":{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"Opaque pagination cursor returned in a previous response's `next_cursor` field."},"LimitParam":{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"Maximum number of items to return per page."}},"schemas":{"ErrorEnvelope":{"type":"object","required":["error"],"properties":{"error":{"$ref":"#/components/schemas/ErrorBody"}}},"ErrorBody":{"type":"object","required":["code","message","retryable"],"properties":{"code":{"type":"string","enum":["validation_error","unauthorized","forbidden","not_found","conflict","rate_limited","quota_exceeded","capacity_exceeded","source_invalid","source_unsupported","job_not_cancelable","output_not_ready","output_persistence_failed","internal_error"]},"message":{"type":"string"},"retryable":{"type":"boolean","description":"True for rate_limited, capacity_exceeded, and internal_error.\nClients should retry with exponential backoff when true.\n"},"request_id":{"type":"string","nullable":true,"description":"Server-assigned request ID for correlation with logs."}}},"JobMetadata":{"type":"object","additionalProperties":true,"description":"Arbitrary JSON object attached to a job. Keys and values are passed\nthrough unchanged to webhook deliveries and echoed on job reads. The\nserialized representation must not exceed 16 KiB. Free-form; SDKs\nsurface this as a `Record<string, unknown>` / `dict[str, Any]` /\n`map[string]interface{}` depending on language.\n"},"WebhookDeliveryPayload":{"type":"object","description":"Payload delivered to registered webhook endpoints on terminal job events.\nSigned with HMAC-SHA256 over `\"{timestamp}.{raw_body}\"` using the\nper-webhook secret. Consumers must verify the signature before processing.\n","required":["event_id","type","timestamp","job_id","status","metadata"],"properties":{"event_id":{"type":"string","description":"Unique delivery event ID for deduplication.","example":"evt_01JQabc123"},"type":{"type":"string","enum":["job.completed","job.failed","job.canceled"],"description":"Event type."},"timestamp":{"type":"string","format":"date-time","description":"ISO 8601 timestamp of the event."},"job_id":{"type":"string","description":"The job that reached a terminal state.","example":"job_01JQ8C4K4W6YQ7M4M0A5K9T6RF"},"status":{"type":"string","enum":["completed","failed","canceled"],"description":"Terminal job status."},"metadata":{"allOf":[{"$ref":"#/components/schemas/JobMetadata"}],"description":"Opaque metadata from the original job submission."}}},"JobProfile":{"type":"string","description":"Encoding profile ID. Coffee-themed naming: prep time maps to encode\nspeed (espresso = fast, cortado = medium, americano = slow). The\nnaming suffixes compose:\n\n- bare name → 8-bit HEVC Main (universal decoder compatibility,\n  default)\n- `-10bit` suffix → HEVC Main10 output. Requires a decoder that\n  supports Main10 (modern phones, modern TVs, Safari, Chrome\n  with hardware decode). Older / low-end devices may not play\n  Main10 output. Pick only when you know the downstream\n  pipeline supports it.\n- `-hq` suffix → quality-biased 8-bit variant for heavy source\n  formats (ProRes, DNxHD, high-bitrate camera originals,\n  mastering-grade intermediates). Files will be larger than\n  the standard tier; pick when preserving detail matters more\n  than bitrate. Broad device compatibility (8-bit Main).\n- `-hq-10bit` suffix → combines HQ with Main10 to preserve\n  10-bit depth end-to-end. Same Main10 compatibility caveat\n  as above; pick for ProRes 422/4444, DNxHD, BRAW, or camera\n  masters where detail AND bit depth matter and you control\n  the downstream pipeline.\n\n**For broad audience playback, pick `sophon-auto` or an explicit\n8-bit coffee profile.** `sophon-auto` produces 8-bit Main output\nregardless of source bit depth.\n\nIf you're not sure which to pick, use `sophon-auto` — the API\npicks per-source settings tuned for consistent output regardless\nof what you submit, and automatically re-encodes at stricter\nsettings if the first pass doesn't hold up.\n\n**8-bit (standard, default):**\n- `sophon-espresso` — fastest, lowest compression\n- `sophon-cortado` — balanced speed and quality\n- `sophon-americano` — slowest, highest compression\n\n**8-bit HQ** (max quality preservation for heavy formats):\n- `sophon-espresso-hq`\n- `sophon-cortado-hq`\n- `sophon-americano-hq`\n\n**10-bit (HEVC Main10):**\n- `sophon-espresso-10bit`\n- `sophon-cortado-10bit`\n- `sophon-americano-10bit`\n\n**10-bit HQ** (max quality preservation AND preserves 10-bit depth):\n- `sophon-espresso-hq-10bit`\n- `sophon-cortado-hq-10bit`\n- `sophon-americano-hq-10bit`\n\n**Adaptive dispatcher:**\n- `sophon-auto` — content-adaptive. The API probes each source,\n  picks tuned settings, and re-encodes at stricter settings if\n  the first pass doesn't hold up. `profile` on the job response\n  stays `sophon-auto`; `effective_profile_id` records the\n  concrete variant the API actually ran.\n","enum":["sophon-espresso","sophon-cortado","sophon-americano","sophon-espresso-hq","sophon-cortado-hq","sophon-americano-hq","sophon-espresso-10bit","sophon-cortado-10bit","sophon-americano-10bit","sophon-espresso-hq-10bit","sophon-cortado-hq-10bit","sophon-americano-hq-10bit","sophon-auto"],"example":"sophon-auto"},"JobSourceType":{"type":"string","enum":["upload"],"description":"Discriminator for `JobSource` variants."},"UploadJobSource":{"type":"object","description":"Source backed by a completed chunked upload session.","required":["type","upload_id"],"properties":{"type":{"$ref":"#/components/schemas/JobSourceType"},"upload_id":{"type":"string","description":"ID of a completed upload session.","example":"upl_01JQ8abc123"}}},"JobSource":{"description":"Source reference. A tagged union with a `type` discriminator.\nLaunch-supported types:\n- `upload` — completed chunked upload session.\n","oneOf":[{"$ref":"#/components/schemas/UploadJobSource"}],"discriminator":{"propertyName":"type","mapping":{"upload":"#/components/schemas/UploadJobSource"}},"example":{"type":"upload","upload_id":"upl_01JQ8abc123"}},"OutputContainer":{"type":"string","enum":["mp4","mkv"],"default":"mp4","description":"Output container format. MP4 is widely compatible; MKV\nsupports a broader range of audio codecs for passthrough.\n"},"CreateJobOutputOptions":{"type":"object","description":"Optional output shaping knobs for a new job.","properties":{"container":{"$ref":"#/components/schemas/OutputContainer"},"audio":{"type":"boolean","default":false,"description":"When true, audio is included in the output. MKV preserves\nsource audio streams unchanged. MP4 preserves broadly compatible\nsource audio codecs when possible, and may normalize\nincompatible codecs to AAC for playback compatibility.\nWhen false, the output is video only.\n"},"target_height":{"type":"integer","minimum":144,"maximum":4320,"nullable":true,"description":"Target output height in pixels. When set, output is scaled down\n(aspect ratio preserved, width derived from source, both dims\nrounded to even). If absent or larger than source height, output\nuses source dimensions. Billing tier is determined by the actual\nencoded output, not by this requested value.\n","example":1080}}},"CreateJobRequest":{"type":"object","required":["source","profile"],"properties":{"source":{"$ref":"#/components/schemas/JobSource"},"profile":{"$ref":"#/components/schemas/JobProfile"},"output":{"$ref":"#/components/schemas/CreateJobOutputOptions"},"webhook_ids":{"type":"array","items":{"type":"string"},"default":[],"description":"IDs of registered webhook endpoints to notify on job state changes."},"metadata":{"allOf":[{"$ref":"#/components/schemas/JobMetadata"}],"description":"Arbitrary key-value metadata attached to the job. Max 16 KiB serialized.","default":{}}}},"JobStatus":{"type":"string","enum":["queued","probing","encoding","muxing","uploading_output","completed","failed","canceled"],"description":"Lifecycle status for an encoding job."},"JobResponse":{"type":"object","required":["id","status","attempt","retryable","profile","source","progress","output","metadata","created_at"],"properties":{"id":{"type":"string","example":"job_abc123def456"},"status":{"$ref":"#/components/schemas/JobStatus"},"status_reason":{"type":"string","nullable":true},"attempt":{"type":"integer"},"retryable":{"type":"boolean","description":"Whether the job can still be retried (attempt < max_attempts and not terminal)."},"profile":{"allOf":[{"$ref":"#/components/schemas/JobProfile"}],"description":"Public profile ID submitted by the customer. For adaptive jobs this\nstays `sophon-auto`; see `effective_profile_id` for the worker's\nresolved concrete profile.\n"},"effective_profile_id":{"type":"string","nullable":true,"description":"Concrete profile resolved by the worker. Omitted until dispatch\nresolves. On explicit-profile jobs this equals `profile`. On\n`sophon-auto` jobs this is a variant identifier recording\nwhich path the API routed the source through; exact encoder\nsettings for a given variant may be updated between releases\nas the adaptive logic is tuned.\n"},"source":{"$ref":"#/components/schemas/JobSourceInfo"},"progress":{"$ref":"#/components/schemas/JobProgress"},"output":{"$ref":"#/components/schemas/JobOutputInfo"},"metadata":{"$ref":"#/components/schemas/JobMetadata"},"created_at":{"type":"string","format":"date-time"},"started_at":{"type":"string","format":"date-time","nullable":true},"completed_at":{"type":"string","format":"date-time","nullable":true},"error":{"type":"string","nullable":true}}},"JobSourceInfo":{"type":"object","required":["sha256"],"properties":{"name":{"type":"string","nullable":true,"description":"Original file name of the source."},"bytes":{"type":"integer","format":"int64","nullable":true},"sha256":{"type":"string","description":"SHA-256 hex digest of the source file."},"duration_seconds":{"type":"number","format":"double","nullable":true},"resolution":{"type":"string","nullable":true,"example":"1920x1080"},"frame_rate":{"type":"string","nullable":true,"example":"23.976"}}},"JobProgress":{"type":"object","required":["phase","percent"],"properties":{"stage":{"type":"string","nullable":true,"description":"Current processing stage label (e.g. \"probing\", \"encoding\", \"muxing\")."},"phase":{"allOf":[{"$ref":"#/components/schemas/JobStatus"}],"description":"Canonical pipeline phase used for progress semantics."},"percent":{"type":"number","format":"float","minimum":0,"maximum":100},"phase_percent":{"type":"number","format":"float","minimum":0,"maximum":100,"nullable":true,"description":"Progress within the current phase. Null before active processing."},"fps":{"type":"number","format":"float","nullable":true},"eta_seconds":{"type":"integer","nullable":true},"frames_done":{"type":"integer","nullable":true},"frames_total":{"type":"integer","nullable":true}}},"JobOutputInfo":{"type":"object","required":["state","container","audio"],"properties":{"state":{"type":"string","enum":["pending","available"]},"container":{"type":"string","description":"Output container format (\"mp4\" or \"mkv\")."},"audio":{"type":"boolean","description":"Whether the output file actually contains audio.\nReflects the muxed result, not the request flag — a\nvideo-only source with audio requested will report false.\n"},"target_height":{"type":"integer","nullable":true,"description":"Customer-requested output height, echoed back. Null when the\njob ran at source dimensions (passthrough).\n","example":1080},"width":{"type":"integer","nullable":true,"description":"Actual encoded output width in pixels (post-ffprobe). Null until\nthe job completes or if the probe failed.\n","example":1920},"height":{"type":"integer","nullable":true,"description":"Actual encoded output height in pixels. See `width`.","example":1080},"bytes":{"type":"integer","format":"int64","nullable":true},"sha256":{"type":"string","nullable":true},"retention_expires_at":{"type":"string","format":"date-time","nullable":true}}},"ListJobsResponse":{"type":"object","required":["jobs","has_more"],"properties":{"jobs":{"type":"array","items":{"$ref":"#/components/schemas/JobResponse"}},"next_cursor":{"type":"string","nullable":true,"description":"Opaque cursor for the next page. Null when no more results."},"has_more":{"type":"boolean"}}},"CreateUploadRequest":{"type":"object","required":["file_name","file_size","mime_type"],"properties":{"file_name":{"type":"string","minLength":1},"file_size":{"type":"integer","format":"int64","minimum":1,"description":"Total file size in bytes."},"mime_type":{"type":"string"}}},"CreateUploadResponse":{"type":"object","required":["id","chunk_size","total_chunks","expires_at"],"properties":{"id":{"type":"string","example":"upl_abc123def456"},"chunk_size":{"type":"integer","format":"int64","description":"Size of each chunk in bytes. Tiered by file size:\n<64 MB = whole file, <=1 GB = 8 MB, <=10 GB = 16 MB, >10 GB = 32 MB.\n"},"total_chunks":{"type":"integer","format":"int64"},"expires_at":{"type":"string","format":"date-time","description":"Upload session expiry (24 hours from creation)."}}},"UploadPartResponse":{"type":"object","required":["part_number","received"],"properties":{"part_number":{"type":"integer","format":"int64"},"received":{"type":"boolean"}}},"UploadStatusResponse":{"type":"object","required":["id","status","file_name","total_chunks","received_chunks","expires_at"],"properties":{"id":{"type":"string"},"status":{"type":"string","enum":["uploading","assembling","completed","canceled"]},"file_name":{"type":"string"},"total_chunks":{"type":"integer"},"received_chunks":{"type":"array","items":{"type":"integer"},"description":"Array of 0-indexed part numbers that have been received."},"expires_at":{"type":"string","format":"date-time"},"source_width":{"type":"integer","nullable":true,"description":"Source media width in pixels, populated from ffprobe after upload\nassembly. Null for uploads in `initiated`/`uploading` state or\nwhen probe failed.\n","example":3840},"source_height":{"type":"integer","nullable":true,"description":"Source media height in pixels. See `source_width`.","example":2160},"source_duration_seconds":{"type":"number","format":"float","nullable":true,"description":"Source media duration in seconds, from ffprobe after upload\nassembly. Used by the webapp free-tier budget check to compute\nrealistic billable_seconds (5-second ceiling rounding).\n","example":600}}},"CompleteUploadResponse":{"type":"object","required":["id","status","sha256","bytes"],"properties":{"id":{"type":"string"},"status":{"type":"string","enum":["completed"]},"sha256":{"type":"string","description":"SHA-256 hex digest of the assembled file."},"bytes":{"type":"integer","format":"int64"}}},"CreateWebhookRequest":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","description":"HTTPS URL to receive webhook deliveries. Must not point to\nprivate, loopback, link-local, or multicast addresses (SSRF prevention).\n"},"name":{"type":"string","nullable":true}}},"WebhookResponse":{"type":"object","required":["id","url","secret","active","created_at"],"properties":{"id":{"type":"string","example":"wh_abc123def456"},"url":{"type":"string","format":"uri"},"secret":{"type":"string","description":"HMAC signing secret (64-char hex string, 32 random bytes).\nOnly returned on creation. Use to verify webhook delivery signatures.\n"},"active":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"}}},"WebhookListItem":{"type":"object","required":["id","url","active","created_at"],"properties":{"id":{"type":"string"},"url":{"type":"string","format":"uri"},"active":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"}}},"WebhookListResponse":{"type":"object","required":["webhooks"],"properties":{"webhooks":{"type":"array","items":{"$ref":"#/components/schemas/WebhookListItem"}}}},"ReadyResponse":{"type":"object","required":["ready"],"properties":{"ready":{"type":"boolean"},"checks_failed":{"type":"array","items":{"type":"string"},"description":"Names of failed readiness checks (database, disk_critical, draining, workers_dead)."}}}}},"webhooks":{"jobTerminalEvent":{"post":{"operationId":"receiveJobTerminalWebhook","summary":"Receive a terminal job webhook","description":"Outbound webhook delivery sent to registered webhook endpoints when a\njob reaches `completed`, `failed`, or `canceled`. Consumers should\nverify `X-Turbo-Signature-256` before processing.\n","tags":["Webhook Events"],"parameters":[{"name":"X-Turbo-Signature-256","in":"header","required":true,"schema":{"type":"string"},"description":"`sha256={hex}` — HMAC-SHA256 of `\"{X-Turbo-Timestamp}.{raw_body}\"`\nusing the webhook's secret key.\n","example":"sha256=abc123def456..."},{"name":"X-Turbo-Event-Id","in":"header","required":true,"schema":{"type":"string"},"description":"Unique event ID for consumer deduplication.","example":"evt_01JQabc123"},{"name":"X-Turbo-Timestamp","in":"header","required":true,"schema":{"type":"string","format":"date-time"},"description":"Timestamp used in signature computation. Check for replay."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookDeliveryPayload"}}}},"responses":{"200":{"description":"Webhook accepted by the consumer."}}}}},"paths":{"/v1/jobs":{"post":{"operationId":"createJob","summary":"Submit an encoding job","description":"Creates a queued encoding job from a completed upload source.\n\n**Picking `profile`:**\n- Use `sophon-auto` unless you have a specific reason not to. It\n  picks per-source settings tuned for consistent output and\n  re-encodes at stricter settings if the first pass doesn't\n  hold up.\n- Use an explicit coffee profile (`sophon-espresso` / `-cortado` /\n  `-americano`) when you want deterministic encoder behavior —\n  same settings regardless of source.\n- Use an `-hq` variant when the source is a heavy format\n  (ProRes, DNxHD, high-bitrate camera originals). Larger output\n  files, maximum detail preservation.\n- Use an `-hq-10bit` variant when the source is 10-bit and you\n  want to preserve that depth end-to-end (ProRes 422/4444,\n  DNxHD, BRAW, camera masters).\n\nSee `JobProfile` for the full enum.\n\n`output.target_height` requests an aspect-preserving downscale\n(width derived from source, both dims rounded to even). If absent\nor larger than source, output uses source dimensions.\n","tags":["Jobs"],"x-codeSamples":[{"lang":"TypeScript","label":"SDK create job from upload","source":"import { randomUUID } from \"node:crypto\";\nimport {\n  JobProfile,\n  JobSource,\n  JobsApi,\n} from \"@liqhtworks/sophon-sdk\";\n\nconst jobs = new JobsApi(config);\nconst job = await jobs.createJob({\n  idempotencyKey: randomUUID(),\n  createJobRequest: {\n    source: JobSource.upload(uploadId),\n    profile: JobProfile.SOPHON_AUTO,\n    output: { container: \"mp4\", audio: true },\n  },\n});\n\n// Persist jobId so the next cron tick polls instead of\n// re-dispatching. `null` means \"not dispatched yet\".\nawait videoRepository.update({ id: video.id }, {\n  sophonState: { ...state, jobId: job.id },\n});\n"},{"lang":"curl","label":"curl","source":"curl -X POST https://api.liqhtworks.xyz/v1/jobs \\\n  -H \"Authorization: Bearer $SOPHON_API_KEY\" \\\n  -H \"Idempotency-Key: video:123:v1:create-job\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"source\":  { \"type\": \"upload\", \"upload_id\": \"upl_…\" },\n    \"profile\": \"sophon-auto\",\n    \"output\":  { \"container\": \"mp4\", \"audio\": true }\n  }'\n"}],"security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateJobRequest"}}}},"responses":{"201":{"description":"Job created.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobResponse"}}}},"400":{"description":"Validation error (bad profile, source, container, metadata, or webhook_ids).","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Missing or invalid credentials.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Insufficient scope (requires jobs:create).","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Idempotency conflict (same key, different request body).","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Source is invalid or unsupported.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limited or quota exceeded.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"get":{"operationId":"listJobs","summary":"List jobs with cursor pagination","description":"Returns jobs for the authenticated organization ordered by creation\ntime, with optional status filtering and opaque cursor pagination.\n","tags":["Jobs"],"security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"name":"status","in":"query","required":false,"schema":{"$ref":"#/components/schemas/JobStatus"},"description":"Filter by job status."},{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/CursorParam"}],"responses":{"200":{"description":"Paginated list of jobs, ordered by created_at descending.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListJobsResponse"}}}},"400":{"description":"Invalid status filter or cursor.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Missing or invalid credentials.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Insufficient scope (requires jobs:read).","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/jobs/{id}":{"get":{"operationId":"getJob","summary":"Get a single job by ID","description":"Returns current job state, progress, source metadata, resolved adaptive\nprofile information, and output availability for one job.\n","tags":["Jobs"],"security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Job details.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobResponse"}}}},"401":{"description":"Missing or invalid credentials.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Insufficient scope (requires jobs:read).","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Job not found.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"delete":{"operationId":"cancelJob","summary":"Cancel a job","description":"Cancels a job in a non-terminal state (queued, probing, encoding, muxing,\nuploading_output). Returns 409 if the job is already completed, failed, or canceled.\n","tags":["Jobs"],"security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Job canceled. Returns the updated job.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobResponse"}}}},"401":{"description":"Missing or invalid credentials.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Insufficient scope (requires jobs:cancel).","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Job not found.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Job is in a terminal state and cannot be canceled (job_not_cancelable).","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/jobs/{id}/output":{"get":{"operationId":"getJobOutput","summary":"Get the encoded output file","description":"Returns a 302 redirect to a signed download URL for the job's output file.\nThe signed URL is valid for 24 hours.\n","tags":["Jobs"],"x-codeSamples":[{"lang":"TypeScript","label":"SDK poll, resolve 302, download","source":"import { writeFile } from \"node:fs/promises\";\nimport { JobStatus, waitForJob } from \"@liqhtworks/sophon-sdk\";\n\nconst final = await waitForJob({\n  api: jobs,\n  jobId,\n  timeoutMs: 30 * 60 * 1000,\n});\nif (final.status !== JobStatus.COMPLETED) {\n  throw new Error(`job ended in ${final.status}`);\n}\n\n// Use raw fetch so redirects are explicit. The SDK intentionally\n// does not follow the 302 and buffer the whole output in memory.\nconst redirect = await fetch(`${basePath}/v1/jobs/${final.id}/output`, {\n  headers: { authorization: `Bearer ${apiKey}` },\n  redirect: \"manual\",\n});\nif (redirect.status !== 302) {\n  throw new Error(`expected 302, got ${redirect.status}`);\n}\nconst location = redirect.headers.get(\"location\");\nif (!location) throw new Error(\"missing output redirect\");\n\nconst download = await fetch(new URL(location, basePath));\nawait writeFile(\"sophon-output.mp4\", Buffer.from(await download.arrayBuffer()));\n"},{"lang":"curl","label":"curl (follow 302 and save)","source":"# -L follows the 302 to the presigned URL and streams the\n# output to stdout; here we pipe to a file.\ncurl -L -o encoded.mp4 \\\n  -H \"Authorization: Bearer $SOPHON_API_KEY\" \\\n  https://api.liqhtworks.xyz/v1/jobs/job_abc123/output\n"}],"security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"302":{"description":"Redirect to signed download URL. The resolved resource is a video/mp4 file.\n","headers":{"Location":{"schema":{"type":"string","format":"uri"},"description":"Signed download URL (24-hour TTL). Resolves to video/mp4."},"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}}},"401":{"description":"Missing or invalid credentials.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Insufficient scope (requires outputs:read).","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Job not found.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Output not ready (job not in completed state).","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/uploads":{"post":{"operationId":"createUpload","summary":"Initialize a chunked upload session","description":"Starts a resumable source upload and returns the chunk size, chunk\ncount, session ID, and expiration timestamp.\n","tags":["Uploads"],"x-codeSamples":[{"lang":"TypeScript","label":"SDK upload helper","source":"import { Blob } from \"node:buffer\";\nimport { readFile } from \"node:fs/promises\";\nimport {\n  UploadsApi,\n  uploadFile,\n} from \"@liqhtworks/sophon-sdk\";\n\nconst uploads = new UploadsApi(config);\nconst bytes = await readFile(\"input.mp4\");\nconst upload = await uploadFile({\n  api: uploads,\n  source: new Blob([bytes], { type: \"video/mp4\" }),\n  fileName: \"input.mp4\",\n  mimeType: \"video/mp4\",\n  concurrency: 4,\n  onProgress: (p) => console.log(`${p.partsDone}/${p.partsTotal} parts`),\n});\n\n// Pass this to createJob as JobSource.upload(upload.uploadId).\nconsole.log(upload.uploadId);\n"},{"lang":"curl","label":"curl (create session only)","source":"curl -X POST https://api.liqhtworks.xyz/v1/uploads \\\n  -H \"Authorization: Bearer $SOPHON_API_KEY\" \\\n  -H \"Idempotency-Key: video:123:v1:create-upload\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"file_name\": \"input.mp4\",\n    \"file_size\": 268435456,\n    \"mime_type\": \"video/mp4\"\n  }'\n"}],"security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUploadRequest"}}}},"responses":{"201":{"description":"Upload session created.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUploadResponse"}}}},"400":{"description":"Validation error (empty file_name, zero file_size, exceeds max).","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Missing or invalid credentials.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Insufficient scope (requires uploads:create).","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Idempotency conflict.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limited or quota exceeded.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"503":{"description":"Disk capacity exceeded; uploads paused.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/uploads/{id}":{"get":{"operationId":"getUpload","summary":"Get upload session status","description":"Returns received chunks and, after assembly/probe, source dimensions\nand duration used by downstream budget and encoding decisions.\n","tags":["Uploads"],"security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Upload session status.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadStatusResponse"}}}},"401":{"description":"Missing or invalid credentials.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Insufficient scope (requires uploads:read).","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Upload session not found.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"delete":{"operationId":"cancelUpload","summary":"Cancel an upload session","description":"Sets the upload session to canceled and deletes staged chunks from disk.\n","tags":["Uploads"],"security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Upload canceled. No content returned.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}}},"401":{"description":"Missing or invalid credentials.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Insufficient scope (requires uploads:write).","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Upload session not found.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/uploads/{id}/parts/{part_number}":{"put":{"operationId":"uploadPart","summary":"Upload a single chunk","description":"Streams the chunk body to disk. Part numbers are 0-indexed.\nUploading the same part number again is idempotent (returns success without re-writing).\n","tags":["Uploads"],"security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"part_number","in":"path","required":true,"schema":{"type":"integer","minimum":0}}],"requestBody":{"required":true,"content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"responses":{"200":{"description":"Chunk received.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadPartResponse"}}}},"400":{"description":"Part number out of range.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Missing or invalid credentials.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Insufficient scope (requires uploads:write).","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Upload session not found.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Upload session is not in \"uploading\" state.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"503":{"description":"Disk capacity exceeded.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/uploads/{id}/complete":{"post":{"operationId":"completeUpload","summary":"Finalize a chunked upload","description":"Assembles all received chunks into a single file, validates size matches\nthe declared file_size, probes with ffprobe, and transitions the session to completed.\n","tags":["Uploads"],"security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"Upload assembled and validated.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompleteUploadResponse"}}}},"400":{"description":"Not all chunks received.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Missing or invalid credentials.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Insufficient scope (requires uploads:write).","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Upload session not found.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Upload session is not in \"uploading\" state.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Assembled file size mismatch or not a valid video file (source_invalid).","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"503":{"description":"Disk capacity exceeded; cannot assemble.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/webhooks":{"post":{"operationId":"createWebhook","summary":"Register a webhook endpoint","description":"Registers an HTTPS endpoint for terminal job events and returns the\nHMAC signing secret once at creation time.\n","tags":["Webhooks"],"security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWebhookRequest"}}}},"responses":{"201":{"description":"Webhook registered. The response includes the HMAC secret (shown only once).","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookResponse"}}}},"400":{"description":"Invalid URL (non-HTTPS, private IP, userinfo, etc.).","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Missing or invalid credentials.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Insufficient scope (requires webhooks:manage).","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Idempotency conflict.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Rate limited.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"get":{"operationId":"listWebhooks","summary":"List active webhook endpoints","description":"Lists active webhook endpoints for the authenticated organization.\n","tags":["Webhooks"],"security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"responses":{"200":{"description":"List of active webhooks for the authenticated organization.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookListResponse"}}}},"401":{"description":"Missing or invalid credentials.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Insufficient scope (requires webhooks:manage).","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/webhooks/{id}":{"delete":{"operationId":"deleteWebhook","summary":"Soft-delete a webhook endpoint","description":"Sets the webhook to inactive. It will no longer receive deliveries.","tags":["Webhooks"],"security":[{"bearerApiKey":[]},{"sessionCookie":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Webhook deactivated. No content returned.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}}},"401":{"description":"Missing or invalid credentials.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Insufficient scope (requires webhooks:manage).","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Webhook not found or already inactive.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/downloads/{token}":{"get":{"operationId":"download","summary":"Download an output file via signed token","description":"Serves the encoded output file. No Authorization header required -- the\nHMAC-signed token (generated by GET /v1/jobs/{id}/output) IS the credential.\nTokens expire after 24 hours.\n","tags":["Downloads"],"security":[],"parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string"},"description":"HMAC-signed download token encoding the object key and expiry."}],"responses":{"200":{"description":"File stream.","headers":{"Content-Disposition":{"schema":{"type":"string"},"description":"attachment; filename=\"<output-filename>\""},"Content-Type":{"schema":{"type":"string"},"description":"video/mp4"},"Content-Length":{"schema":{"type":"integer","format":"int64"}},"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"video/mp4":{"schema":{"type":"string","format":"binary"}}}},"403":{"description":"Invalid or expired download token.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Output file not found on disk.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/healthz":{"get":{"operationId":"healthz","summary":"Liveness probe","description":"Always returns 200. Used by load balancers and orchestrators.","tags":["Health"],"security":[],"responses":{"200":{"description":"Service is alive.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}}}}}},"/readyz":{"get":{"operationId":"readyz","summary":"Readiness probe","description":"Returns 200 when the service is ready to accept traffic. Checks database\nconnectivity, disk headroom (warning and critical thresholds), worker liveness,\nand drain state.\n","tags":["Health"],"security":[],"responses":{"200":{"description":"All readiness checks passed.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReadyResponse"}}}},"503":{"description":"One or more readiness checks failed.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReadyResponse"}}}}}}},"/metrics":{"get":{"operationId":"metrics","summary":"Prometheus metrics (operator-only)","description":"Returns metrics in Prometheus text exposition format.\nThis endpoint is operator-only and is not part of the public customer API.\nIt should be firewalled from external traffic and scraped only by internal\nPrometheus infrastructure.\n","tags":["Health"],"security":[],"x-internal":true,"responses":{"200":{"description":"Prometheus metrics.","content":{"text/plain":{"schema":{"type":"string"}}}}}}}}}