{
  "openapi": "3.1.0",
  "info": {
    "title": "MeshCrunch REST API",
    "version": "1.0.0",
    "description": "MeshCrunch optimises 3D models: simplification, LOD chains, and format\nconversion. This is the REST interface behind the workspace at\n[meshcrunch.com](https://meshcrunch.com) — the same endpoints, the same plan\nlimits, and the same processing credits.\n\n## Authentication\n\nEvery request carries an API key as a bearer token:\n\n```\nAuthorization: Bearer mcrunch_live_1a2b3c4d_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n```\n\nCreate a key in **Studio → Settings → API keys**. Programmatic access is part of\na paid subscription, and entitlement is checked on every request — a key stops\nworking when the subscription behind it lapses, and starts working again when it\nresumes.\n\nA key is minted with a scope. `read` may call every `GET`, and may price an\noperation with `POST /jobs/estimate`; `write` may additionally create assets and\nrun jobs. Each operation below states which it needs. The plaintext key is shown\n**once**, when it is created, and is never recoverable afterwards.\n\nKeys cannot manage keys: creating and revoking them requires a signed-in\nsession, so a leaked key cannot mint a successor or revoke the one you are about\nto use to lock it out.\n\n## How work flows\n\nMesh bytes never travel through this API. Creating an asset is three steps:\n\n1. `POST /uploads` reserves the asset and returns a presigned `PUT` URL.\n2. Your client `PUT`s the file to that URL — direct to object storage.\n3. `POST /uploads/{id}/complete` verifies what actually landed.\n\nThen run work against the asset. `POST /jobs/simplify`, `/jobs/lod` and\n`/jobs/convert` return a `job_id` immediately with status `queued`; poll\n`GET /jobs/{id}` until it reads `done` or `failed`. Results are fetched by\nasking for a download URL at the moment you want the bytes — signed URLs are\nshort-lived, so request one per download rather than storing it.\n\n## Processing credits\n\nEvery job spends credits from the plan's allowance. The cost is one credit per\ndecimation pass — simplify is 1, an N-level LOD chain is N — multiplied by the\nsize band the source file falls in (1× to 25 MB, then 2×, 4×, 8× and 16× to\n1 GB). So the same operation costs more on a larger asset, and a five-level\nchain on a 180 MB source is 20 credits.\n\n`POST /jobs/estimate` returns the exact figure a submission will charge, against\nyour current balance, without writing anything or reserving anything. A\nread-scoped key may call it. Prefer it to arithmetic of your own: the server is\nthe only authority on price, and no request field can name a cost.\n\nUploading costs nothing. Failed jobs are still charged — a reproducibly failing\ninput would otherwise be an unmetered retry loop.\n\n## Retrying safely\n\nEvery job-creation endpoint accepts an optional `client_token`: your own name\nfor one submission, scoped to the asset it runs against. Send the same token\nagain and the original job comes back — nothing is queued and nothing is charged\na second time.\n\nUse it. An automated caller retries on a timeout it cannot distinguish from a\nfailure, and the request most likely to time out is the expensive one. A UUID\ngenerated per attempt is the intended shape; hold it across your retries and\ngenerate a new one when you genuinely mean to run the work again.\n\nThe token names a submission, not a success. Re-running a job that failed is a\nnew decision and needs a new token — otherwise the failed job is what comes back.\n\n## Errors\n\nEvery refusal has the same shape:\n\n```json\n{ \"error\": { \"code\": \"source_expired\", \"message\": \"this asset's retention window has passed\" } }\n```\n\nThe `code` is stable and safe to branch on; the `message` is for a human and may\nbe reworded. `429` means a plan allowance or a rate limit was reached — check\n`GET /auth/usage` to see which.\n",
    "contact": {
      "name": "MeshCrunch support",
      "url": "https://meshcrunch.com/contact"
    }
  },
  "servers": [
    {
      "url": "https://api.meshcrunch.com",
      "description": "Production"
    }
  ],
  "security": [
    {
      "ApiKey": []
    }
  ],
  "tags": [
    {
      "name": "assets",
      "description": "A model you have uploaded, and the thing every job runs against. An asset is reusable: upload once, then simplify it, generate LOD chains from it, and re-crunch it later with different settings."
    },
    {
      "name": "jobs",
      "description": "Processing runs. Creation is asynchronous and returns immediately — poll the job until it settles, then ask for a download URL."
    },
    {
      "name": "account",
      "description": "Who the credential belongs to, and what the plan has left."
    },
    {
      "name": "api-keys",
      "description": "Managing credentials. These require a signed-in session and are refused to an API key, so that a leaked key cannot mint a successor or revoke the key being used to withdraw it."
    },
    {
      "name": "configuration",
      "description": "The limits, formats and features this service enforces for everyone. Read these rather than hardcoding them. No credential required, and nothing here is about the caller."
    }
  ],
  "paths": {
    "/uploads": {
      "post": {
        "tags": [
          "assets"
        ],
        "summary": "Reserve an asset and sign an upload URL",
        "description": "Step one of three. Reserves an asset row and returns a presigned `PUT` URL to transfer the file to. The storage key is chosen by the server and cannot be supplied.\n\n`PUT` the file to `upload_url` with no extra headers, then call `POST /uploads/{id}/complete`. Until you do, the asset is a reservation: it cannot be listed as ready and no job may run against it. The URL expires in ten minutes.\n\nThe declared `size_bytes` is checked against the plan's per-file limit before signing, and the bytes that actually arrive are verified again on completion — a transfer larger than declared is deleted rather than accepted.\n\n**Scope:** requires a `write` API key.",
        "operationId": "createAsset",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateUploadRequest"
              },
              "example": {
                "filename": "chair.glb",
                "size_bytes": 18432100
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UploadUrlResponse"
                },
                "example": {
                  "upload_id": "01a04bec-7b7c-7149-aed2-1001ceb3c1dc",
                  "url": "https://<storage-host>/o/…/original.glb?X-Amz-Signature=…",
                  "method": "PUT",
                  "format": "glb",
                  "max_bytes": 1073741824,
                  "expires_at": "2026-08-29T10:10:00Z"
                }
              }
            }
          },
          "401": {
            "description": "The credential is missing, malformed, revoked, expired, or names no live account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The credential is valid but may not do this — a read-scoped key attempting a change, or an account whose plan no longer includes programmatic access (`paid_plan_required`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The file exceeds the plan's per-file limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "The format is not one this service accepts. `GET /config` publishes the matrix.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request body failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "A plan allowance or a rate limit was reached. `GET /auth/usage` reports which meter is exhausted and when its window resets.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      },
      "get": {
        "tags": [
          "assets"
        ],
        "summary": "List assets",
        "description": "The asset library, ordered by most recent activity rather than by upload time — a model simplified an hour ago ranks above one uploaded yesterday and untouched since. Assets whose retention has lapsed sort last: their rows remain listable until the sweep removes the bytes, but no job may run against them and their sources can no longer be downloaded.\n\n**Scope:** callable with a `read` or `write` API key.",
        "operationId": "listAssets",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 200,
              "minimum": 1,
              "default": 50,
              "title": "Limit"
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0,
              "title": "Offset"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UploadListResponse"
                }
              }
            }
          },
          "401": {
            "description": "The credential is missing, malformed, revoked, expired, or names no live account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The credential is valid but may not do this — a read-scoped key attempting a change, or an account whose plan no longer includes programmatic access (`paid_plan_required`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request body failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "A plan allowance or a rate limit was reached. `GET /auth/usage` reports which meter is exhausted and when its window resets.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/uploads/{upload_id}/complete": {
      "post": {
        "tags": [
          "assets"
        ],
        "summary": "Verify a transferred asset",
        "description": "Step three of three. Confirms the object actually landed, records its verified size, and makes the asset usable.\n\nThe size recorded here is the one everything downstream trusts: it sets the credit multiplier, decides which worker pool runs the job, and counts against stored bytes. A file that arrived larger than the plan allows is removed and the call is refused.\n\n**Scope:** requires a `write` API key.",
        "operationId": "completeAsset",
        "parameters": [
          {
            "name": "upload_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Upload Id"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/CompleteUploadRequest"
                  },
                  {
                    "type": "null"
                  }
                ],
                "title": "Payload"
              },
              "example": {
                "triangle_count": 163558,
                "vertex_count": 82104
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UploadResponse"
                },
                "example": {
                  "id": "01a04bec-7b7c-7149-aed2-1001ceb3c1dc",
                  "status": "ready",
                  "format": "glb",
                  "original_name": "chair.glb",
                  "size_bytes": 18432100,
                  "triangle_count": 163558,
                  "vertex_count": 82104,
                  "created_at": "2026-08-29T10:04:11Z",
                  "expires_at": "2026-11-27T10:04:11Z"
                }
              }
            }
          },
          "401": {
            "description": "The credential is missing, malformed, revoked, expired, or names no live account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The credential is valid but may not do this — a read-scoped key attempting a change, or an account whose plan no longer includes programmatic access (`paid_plan_required`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or it belongs to another account. The two are deliberately indistinguishable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The asset is not ready — its upload was never completed, or it is still being verified.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The file exceeds the plan's per-file limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request body failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "A plan allowance or a rate limit was reached. `GET /auth/usage` reports which meter is exhausted and when its window resets.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/uploads/{upload_id}": {
      "get": {
        "tags": [
          "assets"
        ],
        "summary": "Get one asset",
        "operationId": "getAsset",
        "parameters": [
          {
            "name": "upload_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Upload Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UploadResponse"
                }
              }
            }
          },
          "401": {
            "description": "The credential is missing, malformed, revoked, expired, or names no live account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The credential is valid but may not do this — a read-scoped key attempting a change, or an account whose plan no longer includes programmatic access (`paid_plan_required`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or it belongs to another account. The two are deliberately indistinguishable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request body failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "A plan allowance or a rate limit was reached. `GET /auth/usage` reports which meter is exhausted and when its window resets.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "Everything recorded about one asset, including the geometry counts. Those start out as whatever the uploading client measured and are replaced by the worker's own count the first time a job runs against the asset.\n\n**Scope:** callable with a `read` or `write` API key.",
        "security": [
          {
            "ApiKey": []
          }
        ]
      },
      "delete": {
        "tags": [
          "assets"
        ],
        "summary": "Delete an asset and everything derived from it",
        "description": "Removes the source, every job run against it, and every artifact those jobs published. Irreversible, and the released bytes stop counting against stored storage immediately.\n\nA row is only marked deleted once its objects are actually gone, so a storage failure leaves the asset intact and retryable rather than reporting a success that orphaned the bytes.\n\n**Scope:** requires a `write` API key.",
        "operationId": "deleteAsset",
        "parameters": [
          {
            "name": "upload_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Upload Id"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Successful Response"
          },
          "401": {
            "description": "The credential is missing, malformed, revoked, expired, or names no live account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The credential is valid but may not do this — a read-scoped key attempting a change, or an account whose plan no longer includes programmatic access (`paid_plan_required`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or it belongs to another account. The two are deliberately indistinguishable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request body failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "A plan allowance or a rate limit was reached. `GET /auth/usage` reports which meter is exhausted and when its window resets.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/uploads/{upload_id}/download": {
      "get": {
        "tags": [
          "assets"
        ],
        "summary": "Sign a download URL for the source file",
        "description": "Returns a short-lived URL for the file as it was uploaded, with the original filename attached. Signed by the call rather than stored: request one when you are about to fetch, since a URL held for an hour will have expired.\n\nRefused once the asset's retention window has passed, whether or not the bytes have been swept yet.\n\n**Scope:** callable with a `read` or `write` API key.",
        "operationId": "getAssetDownloadUrl",
        "parameters": [
          {
            "name": "upload_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Upload Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DownloadUrlResponse"
                }
              }
            }
          },
          "401": {
            "description": "The credential is missing, malformed, revoked, expired, or names no live account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The credential is valid but may not do this — a read-scoped key attempting a change, or an account whose plan no longer includes programmatic access (`paid_plan_required`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or it belongs to another account. The two are deliberately indistinguishable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "Retention has passed. `source_expired` means the asset's own window lapsed; `output_expired` means the result's did. The job's status is unaffected: a job that succeeded stays `done` forever, and what it wrote is a lease.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request body failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "A plan allowance or a rate limit was reached. `GET /auth/usage` reports which meter is exhausted and when its window resets.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/uploads/{upload_id}/thumbnail": {
      "get": {
        "tags": [
          "assets"
        ],
        "summary": "Get the asset's preview image",
        "description": "A signed URL for the rendered preview, or the render's current state when there is not one yet. Rendering is queued automatically after an asset is completed; it creates no job and spends no credits. A shaded polygon render, not a material preview.\n\n**Scope:** callable with a `read` or `write` API key.",
        "operationId": "getAssetThumbnail",
        "parameters": [
          {
            "name": "upload_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Upload Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ThumbnailResponse"
                }
              }
            }
          },
          "401": {
            "description": "The credential is missing, malformed, revoked, expired, or names no live account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The credential is valid but may not do this — a read-scoped key attempting a change, or an account whose plan no longer includes programmatic access (`paid_plan_required`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or it belongs to another account. The two are deliberately indistinguishable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request body failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "A plan allowance or a rate limit was reached. `GET /auth/usage` reports which meter is exhausted and when its window resets.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "assets"
        ],
        "summary": "Re-render the asset's preview image",
        "description": "Queues another render for an asset whose preview failed. Refused while one is already in flight. Spends no credits.\n\n**Scope:** requires a `write` API key.",
        "operationId": "retryAssetThumbnail",
        "parameters": [
          {
            "name": "upload_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Upload Id"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ThumbnailResponse"
                }
              }
            }
          },
          "401": {
            "description": "The credential is missing, malformed, revoked, expired, or names no live account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The credential is valid but may not do this — a read-scoped key attempting a change, or an account whose plan no longer includes programmatic access (`paid_plan_required`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or it belongs to another account. The two are deliberately indistinguishable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request body failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "A plan allowance or a rate limit was reached. `GET /auth/usage` reports which meter is exhausted and when its window resets.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/jobs/estimate": {
      "post": {
        "tags": [
          "jobs"
        ],
        "summary": "Price an operation before running it",
        "description": "The exact number of credits a submission will charge, against the balance it will charge them to. **Spends nothing:** no row is written, no credit is reserved, and a quote grants no right to run — so polling it while a caller decides is free.\n\nIt reports `sufficient` rather than refusing an unaffordable operation, because the point is to show the required amount beside the balance while there is still something to change about the request.\n\nA `POST` only because the config it prices is a nested object. It changes nothing, and a read-scoped key may call it.\n\n**Scope:** callable with a `read` or `write` API key.",
        "operationId": "estimateJobCost",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EstimateJobCostRequest"
              },
              "example": {
                "upload_id": "01a04bec-7b7c-7149-aed2-1001ceb3c1dc",
                "operation": "simplify",
                "config": {
                  "mode": "auto"
                }
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobCostEstimateResponse"
                },
                "example": {
                  "operation": "simplify",
                  "upload_id": "01a04bec-7b7c-7149-aed2-1001ceb3c1dc",
                  "source_bytes": 18432100,
                  "base_credits": 1,
                  "size_multiplier": 1,
                  "size_tier": "up to 25 MB",
                  "credits": 1,
                  "credits_limit": 2500,
                  "credits_used": 121,
                  "credits_remaining": 2379,
                  "sufficient": true,
                  "credit_period": "billing_month",
                  "credit_period_ends_at": "2026-09-14T00:00:00Z"
                }
              }
            }
          },
          "401": {
            "description": "The credential is missing, malformed, revoked, expired, or names no live account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The credential is valid but may not do this — a read-scoped key attempting a change, or an account whose plan no longer includes programmatic access (`paid_plan_required`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "Retention has passed. `source_expired` means the asset's own window lapsed; `output_expired` means the result's did. The job's status is unaffected: a job that succeeded stays `done` forever, and what it wrote is a lease.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request body failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "A plan allowance or a rate limit was reached. `GET /auth/usage` reports which meter is exhausted and when its window resets.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/jobs/simplify": {
      "post": {
        "tags": [
          "jobs"
        ],
        "summary": "Simplify an asset, or rebuild it watertight",
        "description": "**One endpoint, two things it can do.** By default it decimates — collapsing triangles while holding the surface inside a measured budget. Set `config.use_alpha_wrap` and it instead rebuilds the model as a **watertight** shell, which is what repairs holes, self-intersections and non-manifold geometry that decimation cannot process. `config.fallback_to_alpha_wrap` (on by default) does the second automatically when the first fails.\n\nQueues the work against a completed asset and returns immediately with a `job_id` and status `queued`. Poll `GET /jobs/{id}`.\n\n**Spends credits:** one decimation pass, multiplied by the source's size band. Price the exact call with `POST /jobs/estimate` first.\n\n`mode: \"auto\"` searches for the smallest mesh that stays inside a measured fidelity budget, and is what most callers want; `mode: \"ratio\"` applies a fixed `target_ratio` instead. Simplification runs per primitive so materials survive, and a result larger than its own source is never published — such a job reports a zero saving rather than a negative one.\n\nSend a `client_token` to make retries safe — the same token against the same asset returns the job the first call created, rather than queueing and charging a second one.\n\n**Scope:** requires a `write` API key.",
        "operationId": "createSimplifyJob",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateSimplifyJobRequest"
              },
              "example": {
                "upload_id": "01a04bec-7b7c-7149-aed2-1001ceb3c1dc",
                "config": {
                  "mode": "auto",
                  "output_format": "glb"
                },
                "client_token": "5f1c2a90-2c1e-4a1e-8f4a-9a1d6b0e1c77"
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobQueuedResponse"
                },
                "example": {
                  "job_id": "01a04c11-2f30-7a55-9c41-77b2e0d9f004",
                  "status": "queued"
                }
              }
            }
          },
          "401": {
            "description": "The credential is missing, malformed, revoked, expired, or names no live account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The credential is valid but may not do this — a read-scoped key attempting a change, or an account whose plan no longer includes programmatic access (`paid_plan_required`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The asset is not ready — its upload was never completed, or it is still being verified.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "Retention has passed. `source_expired` means the asset's own window lapsed; `output_expired` means the result's did. The job's status is unaffected: a job that succeeded stays `done` forever, and what it wrote is a lease.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request body failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "A plan allowance or a rate limit was reached. `GET /auth/usage` reports which meter is exhausted and when its window resets.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "The job could not be queued. The row is persisted, so retry is safe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/jobs/lod": {
      "post": {
        "tags": [
          "jobs"
        ],
        "summary": "Generate an LOD chain, wrapping distant levels if asked",
        "description": "Queues a whole level-of-detail chain as one job, plus a ZIP of every level and a manifest. Every level is decimated from the *source*, never from the level above, so error does not compound down the ladder.\n\n**Spends credits:** one per level, multiplied by the source's size band — so a five-level chain on a 180 MB asset is 20 credits. Atomic: the whole chain succeeds or the whole chain fails, and the charge is decided once at creation.\n\n`config.alpha_wrap_from` rebuilds levels **watertight** from that index down, so a distant LOD can be a closed shell while the near ones keep their materials. It refuses level 0 — a ladder wrapped from the top is no longer the asset you uploaded.\n\nThis is the submission most worth sending a `client_token` with: it is the most expensive to duplicate and the slowest, so the most likely to be retried after a timeout.\n\n**Scope:** requires a `write` API key.",
        "operationId": "createLodJob",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateLodJobRequest"
              },
              "example": {
                "upload_id": "01a04bec-7b7c-7149-aed2-1001ceb3c1dc",
                "config": {
                  "levels": [
                    0.5,
                    0.25,
                    0.1
                  ],
                  "output_format": "glb"
                },
                "client_token": "8c0b1f42-77a3-4e6b-9c2f-3d5a8e4b1a60"
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobQueuedResponse"
                },
                "example": {
                  "job_id": "01a04c11-2f30-7a55-9c41-77b2e0d9f004",
                  "status": "queued"
                }
              }
            }
          },
          "401": {
            "description": "The credential is missing, malformed, revoked, expired, or names no live account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The credential is valid but may not do this — a read-scoped key attempting a change, or an account whose plan no longer includes programmatic access (`paid_plan_required`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The asset is not ready — its upload was never completed, or it is still being verified.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "Retention has passed. `source_expired` means the asset's own window lapsed; `output_expired` means the result's did. The job's status is unaffected: a job that succeeded stays `done` forever, and what it wrote is a lease.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request body failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "A plan allowance or a rate limit was reached. `GET /auth/usage` reports which meter is exhausted and when its window resets.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "The job could not be queued. The row is persisted, so retry is safe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/jobs/convert": {
      "post": {
        "tags": [
          "jobs"
        ],
        "summary": "Convert an asset to another format",
        "description": "Queues a format conversion without intentionally changing topology. `GET /config` publishes which pairs are supported.\n\n**Spends credits:** one pass, multiplied by the source's size band.\n\nNot every pair carries everything. A target that cannot hold materials or textures records a warning on the finished job rather than refusing — check `warnings` on the result. Animated and skinned sources are refused on every pair.\n\nSend a `client_token` to make retries safe.\n\n**Scope:** requires a `write` API key.",
        "operationId": "createConvertJob",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateConvertJobRequest"
              },
              "example": {
                "upload_id": "01a04bec-7b7c-7149-aed2-1001ceb3c1dc",
                "config": {
                  "target_format": "stl"
                },
                "client_token": "b21e7d05-4f0a-4c33-8e10-6d2c9a71f5be"
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobQueuedResponse"
                },
                "example": {
                  "job_id": "01a04c11-2f30-7a55-9c41-77b2e0d9f004",
                  "status": "queued"
                }
              }
            }
          },
          "401": {
            "description": "The credential is missing, malformed, revoked, expired, or names no live account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The credential is valid but may not do this — a read-scoped key attempting a change, or an account whose plan no longer includes programmatic access (`paid_plan_required`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The asset is not ready — its upload was never completed, or it is still being verified.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "Retention has passed. `source_expired` means the asset's own window lapsed; `output_expired` means the result's did. The job's status is unaffected: a job that succeeded stays `done` forever, and what it wrote is a lease.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "The format is not one this service accepts. `GET /config` publishes the matrix.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request body failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "A plan allowance or a rate limit was reached. `GET /auth/usage` reports which meter is exhausted and when its window resets.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "The job could not be queued. The row is persisted, so retry is safe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/jobs": {
      "get": {
        "tags": [
          "jobs"
        ],
        "summary": "List jobs",
        "description": "Every job this account has run, newest first, filterable by status and by source asset. The `active` count covers the whole account and deliberately ignores the filters, so a filtered page still reports how much is genuinely in flight.\n\n**Scope:** callable with a `read` or `write` API key.",
        "operationId": "listJobs",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Status"
            }
          },
          {
            "name": "upload_id",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "format": "uuid"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Upload Id"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 200,
              "minimum": 1,
              "default": 50,
              "title": "Limit"
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0,
              "title": "Offset"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobListResponse"
                }
              }
            }
          },
          "401": {
            "description": "The credential is missing, malformed, revoked, expired, or names no live account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The credential is valid but may not do this — a read-scoped key attempting a change, or an account whose plan no longer includes programmatic access (`paid_plan_required`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request body failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "A plan allowance or a rate limit was reached. `GET /auth/usage` reports which meter is exhausted and when its window resets.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/jobs/{job_id}": {
      "get": {
        "tags": [
          "jobs"
        ],
        "summary": "Get one job",
        "operationId": "getJob",
        "parameters": [
          {
            "name": "job_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Job Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobResponse"
                },
                "example": {
                  "id": "01a04c11-2f30-7a55-9c41-77b2e0d9f004",
                  "upload_id": "01a04bec-7b7c-7149-aed2-1001ceb3c1dc",
                  "job_type": "simplify",
                  "status": "done",
                  "config": {
                    "mode": "auto"
                  },
                  "created_at": "2026-08-29T10:05:02Z",
                  "started_at": "2026-08-29T10:05:04Z",
                  "finished_at": "2026-08-29T10:05:37Z",
                  "expires_at": "2026-11-27T10:05:37Z",
                  "output_availability": "available",
                  "error_code": null,
                  "error_message": null
                }
              }
            }
          },
          "401": {
            "description": "The credential is missing, malformed, revoked, expired, or names no live account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The credential is valid but may not do this — a read-scoped key attempting a change, or an account whose plan no longer includes programmatic access (`paid_plan_required`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or it belongs to another account. The two are deliberately indistinguishable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request body failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "A plan allowance or a rate limit was reached. `GET /auth/usage` reports which meter is exhausted and when its window resets.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "The job's current state. This is the endpoint to poll after creating one: `queued` → `running` → `done` or `failed`.\n\nProgress is reported as elapsed time and a named stage, never a percentage — the worker reports stages rather than fractions, so a percentage would be invented. A failed job carries `error_code` and a plain-language `error_message`.\n\nThere is no cancellation: a queued or running job cannot be stopped.\n\n**Scope:** callable with a `read` or `write` API key.",
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/jobs/{job_id}/result": {
      "get": {
        "tags": [
          "jobs"
        ],
        "summary": "Get a finished job's measurements",
        "description": "What the job produced, in numbers: triangle and vertex counts before and after, output size, and the measured surface deviation where one was taken. Also `warnings` — a closed vocabulary of codes naming what was lost or rebuilt, each with a sentence.\n\nNo download URLs: ask for those separately, so a five-minute credential is not minted for a link nobody clicks.\n\n**Scope:** callable with a `read` or `write` API key.",
        "operationId": "getJobResult",
        "parameters": [
          {
            "name": "job_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Job Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobResultResponse"
                },
                "example": {
                  "job_id": "01a04c11-2f30-7a55-9c41-77b2e0d9f004",
                  "source_triangles": 163558,
                  "output_triangles": 24531,
                  "source_bytes": 18432100,
                  "output_bytes": 3118442,
                  "output_format": "glb",
                  "deviation_mean": 0.0041,
                  "warnings": [
                    {
                      "code": "TANGENTS_DROPPED",
                      "message": "Tangents were not carried."
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The credential is missing, malformed, revoked, expired, or names no live account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The credential is valid but may not do this — a read-scoped key attempting a change, or an account whose plan no longer includes programmatic access (`paid_plan_required`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or it belongs to another account. The two are deliberately indistinguishable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The asset is not ready — its upload was never completed, or it is still being verified.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request body failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "A plan allowance or a rate limit was reached. `GET /auth/usage` reports which meter is exhausted and when its window resets.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/jobs/{job_id}/lod": {
      "get": {
        "tags": [
          "jobs"
        ],
        "summary": "Get an LOD chain's levels",
        "description": "Every level a chain produced, with its ratio and its measured counts. Download individual levels or the bundled ZIP through `GET /jobs/{id}/download`.\n\n**Scope:** callable with a `read` or `write` API key.",
        "operationId": "getLodResult",
        "parameters": [
          {
            "name": "job_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Job Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LodResultResponse"
                }
              }
            }
          },
          "401": {
            "description": "The credential is missing, malformed, revoked, expired, or names no live account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The credential is valid but may not do this — a read-scoped key attempting a change, or an account whose plan no longer includes programmatic access (`paid_plan_required`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or it belongs to another account. The two are deliberately indistinguishable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The asset is not ready — its upload was never completed, or it is still being verified.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request body failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "A plan allowance or a rate limit was reached. `GET /auth/usage` reports which meter is exhausted and when its window resets.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/jobs/{job_id}/download": {
      "get": {
        "tags": [
          "jobs"
        ],
        "summary": "Sign a download URL for a job's output",
        "description": "A short-lived URL for what the job wrote, with a filename derived from the one you uploaded. Use `part=bundle` for an LOD chain's ZIP and `level` for one rung of it.\n\nSigned by the call, so request one when you are about to fetch. Refused once the output's retention window has passed — the job stays `done`, but what it wrote was a lease.\n\n**Scope:** callable with a `read` or `write` API key.",
        "operationId": "getJobDownloadUrl",
        "parameters": [
          {
            "name": "job_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Job Id"
            }
          },
          {
            "name": "level",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "integer",
                  "minimum": 0
                },
                {
                  "type": "null"
                }
              ],
              "title": "Level"
            }
          },
          {
            "name": "part",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^(output|bundle)$",
              "default": "output",
              "title": "Part"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DownloadUrlResponse"
                },
                "example": {
                  "url": "https://<storage-host>/o/…/results/…/output.glb?X-Amz-Signature=…",
                  "expires_in_seconds": 300
                }
              }
            }
          },
          "401": {
            "description": "The credential is missing, malformed, revoked, expired, or names no live account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The credential is valid but may not do this — a read-scoped key attempting a change, or an account whose plan no longer includes programmatic access (`paid_plan_required`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or it belongs to another account. The two are deliberately indistinguishable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The asset is not ready — its upload was never completed, or it is still being verified.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "Retention has passed. `source_expired` means the asset's own window lapsed; `output_expired` means the result's did. The job's status is unaffected: a job that succeeded stays `done` forever, and what it wrote is a lease.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request body failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "A plan allowance or a rate limit was reached. `GET /auth/usage` reports which meter is exhausted and when its window resets.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/auth/me": {
      "get": {
        "tags": [
          "account"
        ],
        "summary": "Who this credential belongs to",
        "description": "The account behind the credential, its plan, and its current allowances. The plan is derived from the live subscription on every request rather than read from a column, so it is never stale.\n\n**Scope:** callable with a `read` or `write` API key.",
        "operationId": "getAccount",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MeResponse"
                }
              }
            }
          },
          "401": {
            "description": "The credential is missing, malformed, revoked, expired, or names no live account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The credential is valid but may not do this — a read-scoped key attempting a change, or an account whose plan no longer includes programmatic access (`paid_plan_required`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request body failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "A plan allowance or a rate limit was reached. `GET /auth/usage` reports which meter is exhausted and when its window resets.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/auth/usage": {
      "get": {
        "tags": [
          "account"
        ],
        "summary": "What the plan has left",
        "description": "All three meters, each with the window it is measured over: processing credits, processed data, and storage held right now.\n\nThe windows differ by plan and are stated in the response rather than assumed — paid plans meter credits over the Paddle billing period, free accounts over a rolling 24 hours. A number without its window is not a limit. This is the endpoint to consult after a `429`.\n\n**Scope:** callable with a `read` or `write` API key.",
        "operationId": "getUsage",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UsageResponse"
                }
              }
            }
          },
          "401": {
            "description": "The credential is missing, malformed, revoked, expired, or names no live account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The credential is valid but may not do this — a read-scoped key attempting a change, or an account whose plan no longer includes programmatic access (`paid_plan_required`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request body failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "A plan allowance or a rate limit was reached. `GET /auth/usage` reports which meter is exhausted and when its window resets.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": []
          }
        ]
      }
    },
    "/auth/api-keys": {
      "post": {
        "tags": [
          "api-keys"
        ],
        "summary": "Create an API key",
        "description": "Mints a key and returns the plaintext **once**. Nothing stores it — only a digest — so there is no endpoint that can show it again, and losing it means creating another.\n\nRequires a paid plan and a signed-in session. Scope defaults to `read`; ask for `write` only if the key will create assets or run jobs. An account may hold 20 live keys.\n\n**Scope:** requires a signed-in session. API keys cannot manage API keys, whatever their scope.",
        "operationId": "createApiKey",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateApiKeyRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreatedApiKeyResponse"
                }
              }
            }
          },
          "401": {
            "description": "The credential is missing, malformed, revoked, expired, or names no live account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The credential is valid but may not do this — a read-scoped key attempting a change, or an account whose plan no longer includes programmatic access (`paid_plan_required`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request body failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "A plan allowance or a rate limit was reached. `GET /auth/usage` reports which meter is exhausted and when its window resets.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "SessionToken": []
          }
        ]
      },
      "get": {
        "tags": [
          "api-keys"
        ],
        "summary": "List API keys",
        "description": "Every key this account holds, newest first, revoked ones included — confirming that something was withdrawn is exactly what this is for. Carries nothing that authenticates: the public prefix identifies a key without being any part of what proves it.\n\n**Scope:** requires a signed-in session. API keys cannot manage API keys, whatever their scope.",
        "operationId": "listApiKeys",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeyListResponse"
                }
              }
            }
          },
          "401": {
            "description": "The credential is missing, malformed, revoked, expired, or names no live account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The credential is valid but may not do this — a read-scoped key attempting a change, or an account whose plan no longer includes programmatic access (`paid_plan_required`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request body failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "A plan allowance or a rate limit was reached. `GET /auth/usage` reports which meter is exhausted and when its window resets.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "SessionToken": []
          }
        ]
      }
    },
    "/auth/api-keys/{key_id}": {
      "delete": {
        "tags": [
          "api-keys"
        ],
        "summary": "Revoke an API key",
        "description": "Withdraws a key. Takes effect on the next request that presents it. Idempotent — revoking an already-revoked key reports the same row rather than failing, since the intent is already satisfied.\n\nThe row is kept and timestamped rather than deleted, so a key that was used and then withdrawn leaves evidence it existed. Available even on a lapsed subscription: losing the ability to revoke your own credentials because you stopped paying would turn a billing event into a security problem.\n\n**Scope:** requires a signed-in session. API keys cannot manage API keys, whatever their scope.",
        "operationId": "revokeApiKey",
        "parameters": [
          {
            "name": "key_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Key Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeySummary"
                }
              }
            }
          },
          "401": {
            "description": "The credential is missing, malformed, revoked, expired, or names no live account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The credential is valid but may not do this — a read-scoped key attempting a change, or an account whose plan no longer includes programmatic access (`paid_plan_required`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such resource, or it belongs to another account. The two are deliberately indistinguishable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The request body failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "A plan allowance or a rate limit was reached. `GET /auth/usage` reports which meter is exhausted and when its window resets.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "SessionToken": []
          }
        ]
      }
    },
    "/config": {
      "get": {
        "tags": [
          "configuration"
        ],
        "summary": "Plan limits, supported formats, and feature availability",
        "description": "What the service enforces for everyone: the plan catalogue with every limit and its window, the format matrix, the credit size ladder, and which features are live.\n\nUnauthenticated, and contains nothing about the caller. Read limits from here rather than hardcoding them — they are operator-editable and this endpoint is where a change becomes visible.\n\n**Scope:** callable with a `read` or `write` API key.",
        "operationId": "getServiceConfig",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConfigResponse"
                }
              }
            }
          },
          "422": {
            "description": "The request body failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": []
      }
    }
  },
  "components": {
    "schemas": {
      "ApiKeyListResponse": {
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/ApiKeySummary"
            },
            "type": "array",
            "title": "Items"
          },
          "total": {
            "type": "integer",
            "title": "Total"
          }
        },
        "type": "object",
        "required": [
          "items",
          "total"
        ],
        "title": "ApiKeyListResponse"
      },
      "ApiKeySummary": {
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "title": "Id"
          },
          "name": {
            "type": "string",
            "title": "Name"
          },
          "prefix": {
            "type": "string",
            "title": "Prefix"
          },
          "scope": {
            "type": "string",
            "title": "Scope"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "title": "Created At"
          },
          "last_used_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Used At"
          },
          "expires_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Expires At"
          },
          "revoked_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Revoked At"
          }
        },
        "type": "object",
        "required": [
          "id",
          "name",
          "prefix",
          "scope",
          "created_at"
        ],
        "title": "ApiKeySummary",
        "description": "One key as a listing shows it. Carries nothing that authenticates."
      },
      "CompleteUploadRequest": {
        "properties": {
          "triangle_count": {
            "anyOf": [
              {
                "type": "integer",
                "minimum": 0
              },
              {
                "type": "null"
              }
            ],
            "title": "Triangle Count",
            "description": "Advisory and optional. It lets a listing describe the asset before any job has run; the worker replaces it with its own count on the first job. Nothing is authorised, billed or routed on it."
          },
          "vertex_count": {
            "anyOf": [
              {
                "type": "integer",
                "minimum": 0
              },
              {
                "type": "null"
              }
            ],
            "title": "Vertex Count",
            "description": "Advisory, like `triangle_count`."
          }
        },
        "type": "object",
        "title": "CompleteUploadRequest",
        "description": "Advisory geometry the browser measured while previewing the model.\n\nBoth fields are optional and *display only*: they populate the asset card\nbefore any job has run, and the first completed job replaces them with the\nworker's own count. Nothing authorises, bills, or routes on them — which is\nwhy a client-reported number is acceptable here and nowhere else in this API."
      },
      "ConfigResponse": {
        "properties": {
          "plans": {
            "items": {
              "additionalProperties": true,
              "type": "object"
            },
            "type": "array",
            "title": "Plans"
          },
          "formats": {
            "items": {
              "additionalProperties": true,
              "type": "object"
            },
            "type": "array",
            "title": "Formats"
          },
          "conversion_pairs": {
            "items": {
              "additionalProperties": true,
              "type": "object"
            },
            "type": "array",
            "title": "Conversion Pairs"
          },
          "features": {
            "items": {
              "additionalProperties": true,
              "type": "object"
            },
            "type": "array",
            "title": "Features"
          },
          "pricing": {
            "additionalProperties": true,
            "type": "object",
            "title": "Pricing"
          },
          "terms_version": {
            "type": "string",
            "title": "Terms Version"
          },
          "privacy_version": {
            "type": "string",
            "title": "Privacy Version"
          }
        },
        "type": "object",
        "required": [
          "plans",
          "formats",
          "conversion_pairs",
          "features",
          "pricing",
          "terms_version",
          "privacy_version"
        ],
        "title": "ConfigResponse",
        "description": "The service describing what it enforces and what it can do.\n\nLoosely typed on purpose: these three lists are assembled by\n``plans.plan_catalog``, ``formats.format_catalog`` and\n``features.feature_catalog``, which are the authorities. Restating their\nshapes here would create a fourth place to update and a fourth chance to\ndisagree, which is the failure this endpoint exists to prevent."
      },
      "ConvertConfig": {
        "properties": {
          "target_format": {
            "type": "string",
            "enum": [
              "obj",
              "stl",
              "ply",
              "off",
              "glb"
            ],
            "title": "Target Format",
            "description": "What to convert to. `GET /config` publishes which pairs are supported and what each carries — a target that cannot hold materials records a warning rather than refusing."
          },
          "output_profile": {
            "type": "string",
            "enum": [
              "universal",
              "compact",
              "minimum"
            ],
            "title": "Output Profile",
            "description": "Which glTF extensions a GLB result may use, which is a compatibility promise rather than a compression level. `universal` declares none and opens anywhere; `compact` and `minimum` are progressively smaller and need a reader that supports the extensions they declare. Ignored for every non-GLB format.",
            "default": "universal"
          }
        },
        "type": "object",
        "required": [
          "target_format"
        ],
        "title": "ConvertConfig",
        "description": "A format-only conversion; source format is read from the validated asset."
      },
      "CreateApiKeyRequest": {
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 120,
            "minLength": 1,
            "title": "Name",
            "description": "What to call the key, so a listing is legible and the right one gets revoked. Name it for where it runs."
          },
          "scope": {
            "type": "string",
            "enum": [
              "read",
              "write"
            ],
            "title": "Scope",
            "description": "`read` may call every GET and may price an operation; `write` may additionally create assets and run jobs. Defaults to the narrower grant deliberately.",
            "default": "read"
          },
          "expires_in_days": {
            "anyOf": [
              {
                "type": "integer",
                "maximum": 365,
                "minimum": 1
              },
              {
                "type": "null"
              }
            ],
            "title": "Expires In Days",
            "description": "Null means the key lasts until it is revoked, which is what most callers want. An expiry is a deliberate choice."
          }
        },
        "type": "object",
        "required": [
          "name"
        ],
        "title": "CreateApiKeyRequest",
        "description": "Ask for a credential a non-browser caller can hold.\n\nNo owner field, and there never may be one: which account a key belongs to\ncomes from the verified session that asked for it. A request naming its own\nowner would be a way to mint a credential for somebody else."
      },
      "CreateConvertJobRequest": {
        "properties": {
          "upload_id": {
            "type": "string",
            "format": "uuid",
            "title": "Upload Id",
            "description": "The completed asset to run this against."
          },
          "config": {
            "$ref": "#/components/schemas/ConvertConfig",
            "description": "What to convert to."
          },
          "client_token": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 128,
                "minLength": 1
              },
              {
                "type": "null"
              }
            ],
            "title": "Client Token",
            "description": "Your own name for this submission, so a retry returns the job the first call created rather than buying the work twice. Scoped to the asset; a UUID generated per attempt is the intended shape. It names a *submission*, not a success — re-running failed work needs a new one."
          }
        },
        "type": "object",
        "required": [
          "upload_id",
          "config"
        ],
        "title": "CreateConvertJobRequest"
      },
      "CreateLodJobRequest": {
        "properties": {
          "upload_id": {
            "type": "string",
            "format": "uuid",
            "title": "Upload Id",
            "description": "The completed asset to run this against."
          },
          "config": {
            "$ref": "#/components/schemas/LodConfig",
            "description": "The chain to build. Every field has a default, so `{}` builds the standard five-level ladder."
          },
          "client_token": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 128,
                "minLength": 1
              },
              {
                "type": "null"
              }
            ],
            "title": "Client Token",
            "description": "Your own name for this submission, so a retry returns the job the first call created rather than buying the work twice. Scoped to the asset; a UUID generated per attempt is the intended shape. It names a *submission*, not a success — re-running failed work needs a new one."
          }
        },
        "type": "object",
        "required": [
          "upload_id"
        ],
        "title": "CreateLodJobRequest"
      },
      "CreateSimplifyJobRequest": {
        "properties": {
          "upload_id": {
            "type": "string",
            "format": "uuid",
            "title": "Upload Id",
            "description": "The completed asset to run this against."
          },
          "config": {
            "$ref": "#/components/schemas/SimplifyConfig",
            "description": "How to simplify it. Every field has a default, so `{}` is a valid request."
          },
          "client_token": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 128,
                "minLength": 1
              },
              {
                "type": "null"
              }
            ],
            "title": "Client Token",
            "description": "Your own name for this submission, so a retry returns the job the first call created rather than buying the work twice. Scoped to the asset; a UUID generated per attempt is the intended shape. It names a *submission*, not a success — re-running failed work needs a new one."
          }
        },
        "type": "object",
        "required": [
          "upload_id"
        ],
        "title": "CreateSimplifyJobRequest"
      },
      "CreateUploadRequest": {
        "properties": {
          "filename": {
            "type": "string",
            "maxLength": 1024,
            "minLength": 1,
            "title": "Filename",
            "description": "The name to store the asset under. Its extension decides how the file is parsed, so it must be one this service accepts."
          },
          "size_bytes": {
            "type": "integer",
            "exclusiveMinimum": 0,
            "title": "Size Bytes",
            "description": "The file's size. Checked against the plan's per-file limit before a URL is signed, and verified again against what actually arrives."
          }
        },
        "type": "object",
        "required": [
          "filename",
          "size_bytes"
        ],
        "title": "CreateUploadRequest",
        "description": "Ask for a presigned PUT. The client names its file; the server names the key."
      },
      "CreatedApiKeyResponse": {
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "title": "Id"
          },
          "name": {
            "type": "string",
            "title": "Name"
          },
          "prefix": {
            "type": "string",
            "title": "Prefix"
          },
          "scope": {
            "type": "string",
            "title": "Scope"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "title": "Created At"
          },
          "last_used_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Used At"
          },
          "expires_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Expires At"
          },
          "revoked_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Revoked At"
          },
          "key": {
            "type": "string",
            "title": "Key"
          }
        },
        "type": "object",
        "required": [
          "id",
          "name",
          "prefix",
          "scope",
          "created_at",
          "key"
        ],
        "title": "CreatedApiKeyResponse",
        "description": "A newly minted key, and **the only time the secret is ever returned.**\n\nNothing stores the plaintext — only a SHA-256 of it — so this response is\nnot a convenience but the single copy. A key that could be read back later\nis one that leaks from the database, the logs, and every backup of both."
      },
      "DownloadUrlResponse": {
        "properties": {
          "url": {
            "type": "string",
            "title": "Url"
          },
          "format": {
            "type": "string",
            "title": "Format"
          },
          "filename": {
            "type": "string",
            "title": "Filename"
          },
          "expires_in_seconds": {
            "type": "integer",
            "title": "Expires In Seconds"
          }
        },
        "type": "object",
        "required": [
          "url",
          "format",
          "filename",
          "expires_in_seconds"
        ],
        "title": "DownloadUrlResponse",
        "description": "A short-lived signed URL for one stored object, plus how to name it."
      },
      "Error": {
        "type": "object",
        "title": "Error",
        "description": "Every refusal from this API has this shape. The `code` is stable and safe to branch on; the `message` is written for a person and may be reworded without notice.",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "description": "A stable machine-readable identifier for this refusal.",
                "examples": [
                  "source_expired"
                ]
              },
              "message": {
                "type": "string",
                "description": "A human-readable explanation. Do not parse it.",
                "examples": [
                  "this asset's retention window has passed"
                ]
              }
            },
            "required": [
              "code",
              "message"
            ]
          }
        },
        "required": [
          "error"
        ]
      },
      "EstimateJobCostRequest": {
        "properties": {
          "upload_id": {
            "type": "string",
            "format": "uuid",
            "title": "Upload Id",
            "description": "The completed asset to run this against."
          },
          "operation": {
            "type": "string",
            "enum": [
              "simplify",
              "lod",
              "convert"
            ],
            "title": "Operation",
            "description": "Which operation to price."
          },
          "config": {
            "additionalProperties": true,
            "type": "object",
            "title": "Config",
            "description": "The same config the matching create endpoint takes, validated identically — so a config that prices here is one that will submit."
          }
        },
        "type": "object",
        "required": [
          "upload_id",
          "operation"
        ],
        "title": "EstimateJobCostRequest",
        "description": "Ask what an operation would cost against an asset, before running it.\n\nOne request shape for every operation rather than one endpoint each, because\nthe answer is the same answer and a caller deciding *which* operation to run\nwants to price several. ``config`` is an untyped mapping here and is\nvalidated by the operation's own model in the route — the same models the\ncreate endpoints use — so a chain that would be refused as invalid cannot be\nquoted either.\n\nNo cost field, and there never may be one: the price is derived server-side\nfrom the validated config and the verified size of the stored object, and a\nrequest that could name its own would be a way to buy compute at a price the\nbuyer chose."
      },
      "JobCostEstimateResponse": {
        "properties": {
          "operation": {
            "type": "string",
            "title": "Operation"
          },
          "upload_id": {
            "type": "string",
            "format": "uuid",
            "title": "Upload Id",
            "description": "The completed asset to run this against."
          },
          "source_bytes": {
            "type": "integer",
            "title": "Source Bytes"
          },
          "base_credits": {
            "type": "integer",
            "title": "Base Credits"
          },
          "size_multiplier": {
            "type": "integer",
            "title": "Size Multiplier"
          },
          "size_tier": {
            "type": "string",
            "title": "Size Tier"
          },
          "complexity_multiplier": {
            "type": "integer",
            "title": "Complexity Multiplier"
          },
          "credits": {
            "type": "integer",
            "title": "Credits"
          },
          "credits_limit": {
            "type": "integer",
            "title": "Credits Limit"
          },
          "credits_used": {
            "type": "integer",
            "title": "Credits Used"
          },
          "credits_remaining": {
            "type": "integer",
            "title": "Credits Remaining"
          },
          "sufficient": {
            "type": "boolean",
            "title": "Sufficient"
          },
          "credit_period": {
            "type": "string",
            "enum": [
              "rolling_24h",
              "calendar_month",
              "billing_month"
            ],
            "title": "Credit Period"
          },
          "credit_period_ends_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Credit Period Ends At"
          }
        },
        "type": "object",
        "required": [
          "operation",
          "upload_id",
          "source_bytes",
          "base_credits",
          "size_multiplier",
          "size_tier",
          "complexity_multiplier",
          "credits",
          "credits_limit",
          "credits_used",
          "credits_remaining",
          "sufficient",
          "credit_period"
        ],
        "title": "JobCostEstimateResponse",
        "description": "What one operation would cost, with the arithmetic and the balance.\n\nThe breakdown is published rather than just the total because a number\nbeside a button is something a customer has to take on trust, and the two\nfactors are exactly what they can act on: pick a smaller source, or ask for\nfewer levels.\n\n**Advisory, and consumes nothing.** No row is written and no credit is\nreserved, so asking twice costs nothing and a quote confers no right to run;\nthe authoritative check happens inside job creation, under the owner lock,\nagainst these same functions. ``sufficient`` is therefore a statement about\nthis instant — it is what lets a client show the shortfall *before*\nenqueueing rather than discovering it as a 429."
      },
      "JobListResponse": {
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/JobSummary"
            },
            "type": "array",
            "title": "Items"
          },
          "total": {
            "type": "integer",
            "title": "Total"
          },
          "active": {
            "type": "integer",
            "title": "Active"
          }
        },
        "type": "object",
        "required": [
          "items",
          "total",
          "active"
        ],
        "title": "JobListResponse"
      },
      "JobQueuedResponse": {
        "properties": {
          "job_id": {
            "type": "string",
            "format": "uuid",
            "title": "Job Id"
          },
          "status": {
            "type": "string",
            "const": "queued",
            "title": "Status"
          }
        },
        "type": "object",
        "required": [
          "job_id",
          "status"
        ],
        "title": "JobQueuedResponse"
      },
      "JobResponse": {
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "title": "Id"
          },
          "upload_id": {
            "type": "string",
            "format": "uuid",
            "title": "Upload Id",
            "description": "The completed asset to run this against."
          },
          "batch_id": {
            "anyOf": [
              {
                "type": "string",
                "format": "uuid"
              },
              {
                "type": "null"
              }
            ],
            "title": "Batch Id"
          },
          "job_type": {
            "type": "string",
            "title": "Job Type"
          },
          "priority": {
            "type": "string",
            "title": "Priority"
          },
          "status": {
            "type": "string",
            "title": "Status"
          },
          "config": {
            "additionalProperties": true,
            "type": "object",
            "title": "Config"
          },
          "result_stats": {
            "anyOf": [
              {
                "additionalProperties": true,
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "title": "Result Stats"
          },
          "started_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Started At"
          },
          "finished_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Finished At"
          },
          "expires_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Expires At"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "title": "Created At"
          },
          "error_message": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Error Message"
          },
          "output_availability": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Output Availability"
          },
          "error_code": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Error Code"
          }
        },
        "type": "object",
        "required": [
          "id",
          "upload_id",
          "batch_id",
          "job_type",
          "priority",
          "status",
          "config",
          "result_stats",
          "started_at",
          "finished_at",
          "expires_at",
          "created_at"
        ],
        "title": "JobResponse"
      },
      "JobResultResponse": {
        "properties": {
          "job_id": {
            "type": "string",
            "format": "uuid",
            "title": "Job Id"
          },
          "output_url": {
            "type": "string",
            "title": "Output Url"
          },
          "output_format": {
            "type": "string",
            "title": "Output Format"
          },
          "output_container": {
            "type": "string",
            "title": "Output Container"
          },
          "output_filename": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Output Filename"
          },
          "preview_url": {
            "type": "string",
            "title": "Preview Url"
          },
          "expires_in_seconds": {
            "type": "integer",
            "title": "Expires In Seconds"
          },
          "input": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/MeshStats"
              },
              {
                "type": "null"
              }
            ]
          },
          "output": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/MeshStats"
              },
              {
                "type": "null"
              }
            ]
          },
          "triangle_ratio": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Triangle Ratio"
          },
          "deviation_mean": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Deviation Mean"
          },
          "output_watertight": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ],
            "title": "Output Watertight"
          },
          "warnings": {
            "items": {
              "$ref": "#/components/schemas/JobWarningResponse"
            },
            "type": "array",
            "title": "Warnings",
            "default": []
          }
        },
        "type": "object",
        "required": [
          "job_id",
          "output_url",
          "output_format",
          "output_container",
          "preview_url",
          "expires_in_seconds",
          "input",
          "output",
          "triangle_ratio"
        ],
        "title": "JobResultResponse",
        "description": "Short-lived download URLs for a finished job's artifacts, plus what changed.\n\nURLs are signed per request rather than stored: one that outlived the\nresponse would be a standing read grant on a private object, and the storage\nkey itself is never handed to a client.\n\nStats ride along so the viewer renders a before/after in a single round trip\ninstead of fetching the job and its result separately."
      },
      "JobSummary": {
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "title": "Id"
          },
          "upload_id": {
            "type": "string",
            "format": "uuid",
            "title": "Upload Id",
            "description": "The completed asset to run this against."
          },
          "upload_name": {
            "type": "string",
            "title": "Upload Name"
          },
          "job_type": {
            "type": "string",
            "title": "Job Type"
          },
          "status": {
            "type": "string",
            "title": "Status"
          },
          "started_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Started At"
          },
          "finished_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Finished At"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "title": "Created At"
          },
          "expires_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Expires At"
          },
          "output_availability": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Output Availability"
          },
          "error_code": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Error Code"
          },
          "error_message": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Error Message"
          }
        },
        "type": "object",
        "required": [
          "id",
          "upload_id",
          "upload_name",
          "job_type",
          "status",
          "started_at",
          "finished_at",
          "created_at"
        ],
        "title": "JobSummary",
        "description": "One row of the jobs table, carrying enough of its asset to be readable.\n\nThe asset's name and id ride along because a job list that only says\n``simplify — done`` is unusable: the first thing anyone wants is *which\nmodel*, and joining it here costs one join rather than N follow-up requests."
      },
      "JobWarningResponse": {
        "properties": {
          "code": {
            "type": "string",
            "title": "Code"
          },
          "message": {
            "type": "string",
            "title": "Message"
          }
        },
        "type": "object",
        "required": [
          "code",
          "message"
        ],
        "title": "JobWarningResponse",
        "description": "One thing a successful job could not do, classified and worded.\n\n``code`` comes from the closed vocabulary in ``job_warnings`` and is what a\nclient branches on; ``message`` is prose written server-side and is rendered\nas-is. A code a client does not recognise — including ``UNSPECIFIED``, which\nis what results stored before the vocabulary existed read back as — must\nstill be displayed, because the sentence is the part the user needs."
      },
      "LodConfig": {
        "properties": {
          "levels": {
            "items": {
              "type": "number",
              "maximum": 1,
              "exclusiveMinimum": 0
            },
            "type": "array",
            "maxItems": 8,
            "minItems": 1,
            "title": "Levels",
            "description": "The rungs of the chain, as fractions of the original triangle count, ordered from most to least detail and all distinct. Every level is decimated from the *source* rather than from the level above, so error does not compound down the ladder — which is also why the job costs one credit per level.",
            "default": [
              1,
              0.6,
              0.35,
              0.18,
              0.08
            ]
          },
          "output_format": {
            "type": "string",
            "enum": [
              "obj",
              "stl",
              "ply",
              "off",
              "glb"
            ],
            "title": "Output Format",
            "description": "What to write every level as. GLB by default: a chain is usually consumed by an engine, and it is the one format here that carries materials in a single file.",
            "default": "glb"
          },
          "output_profile": {
            "type": "string",
            "enum": [
              "universal",
              "compact",
              "minimum"
            ],
            "title": "Output Profile",
            "description": "Which glTF extensions a GLB result may use, which is a compatibility promise rather than a compression level. `universal` declares none and opens anywhere; `compact` and `minimum` are progressively smaller and need a reader that supports the extensions they declare. Ignored for every non-GLB format.",
            "default": "universal"
          },
          "preserve_appearance": {
            "type": "boolean",
            "title": "Preserve Appearance",
            "description": "Keep materials, textures, UVs and vertex colours on every level.",
            "default": true
          },
          "alpha_wrap_from": {
            "anyOf": [
              {
                "type": "integer",
                "minimum": 1
              },
              {
                "type": "null"
              }
            ],
            "title": "Alpha Wrap From",
            "description": "**Rebuild the surface, from this level down.** Levels above the index are decimated normally; this one and every coarser level are replaced by a watertight shell, which is how a distant LOD stays closed on a model that is not. Geometry-only, because a rebuilt surface can carry no attributes. Refuses 0 — a ladder wrapped from the top is no longer the asset you uploaded. Null wraps nothing."
          }
        },
        "type": "object",
        "title": "LodConfig",
        "description": "Validated instructions for one ``lod`` job: a whole chain in one run.\n\nA chain is one job rather than one job per level because the levels are a\nsingle deliverable — the ZIP, the manifest, and the viewer's level selector\nall describe the set. Splitting it would also re-download and re-normalize\nthe same source once per level, which is the expensive part.\n\n``levels`` are *detail retained*, matching ``SimplifyConfig.target_ratio``,\nand are the ratios applied to the source — never cascaded — so error does not\ncompound down the chain."
      },
      "LodLevelResult": {
        "properties": {
          "level": {
            "type": "integer",
            "title": "Level"
          },
          "target_ratio": {
            "type": "number",
            "title": "Target Ratio"
          },
          "triangles": {
            "type": "integer",
            "title": "Triangles"
          },
          "vertices": {
            "type": "integer",
            "title": "Vertices"
          },
          "size_bytes": {
            "type": "integer",
            "title": "Size Bytes"
          },
          "url": {
            "type": "string",
            "title": "Url"
          },
          "format": {
            "type": "string",
            "title": "Format"
          }
        },
        "type": "object",
        "required": [
          "level",
          "target_ratio",
          "triangles",
          "vertices",
          "size_bytes",
          "url",
          "format"
        ],
        "title": "LodLevelResult",
        "description": "One rung of a finished LOD chain, with a URL that expires like any result."
      },
      "LodResultResponse": {
        "properties": {
          "job_id": {
            "type": "string",
            "format": "uuid",
            "title": "Job Id"
          },
          "levels": {
            "items": {
              "$ref": "#/components/schemas/LodLevelResult"
            },
            "type": "array",
            "title": "Levels"
          },
          "bundle_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Bundle Url"
          },
          "expires_in_seconds": {
            "type": "integer",
            "title": "Expires In Seconds"
          },
          "input": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/MeshStats"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "type": "object",
        "required": [
          "job_id",
          "levels",
          "bundle_url",
          "expires_in_seconds",
          "input"
        ],
        "title": "LodResultResponse",
        "description": "Every generated level, plus the one-click bundle.\n\n``bundle_url`` points at a ZIP the *worker* wrote when the job finished, not\none assembled on request: mesh bytes never pass through the API, and\nrebuilding it per download would re-fetch every level from storage to produce\na file that already exists."
      },
      "MeResponse": {
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "user",
              "guest"
            ],
            "title": "Kind"
          },
          "id": {
            "type": "string",
            "format": "uuid",
            "title": "Id"
          },
          "tier": {
            "type": "string",
            "title": "Tier"
          },
          "plan": {
            "type": "string",
            "title": "Plan"
          },
          "email": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Email"
          },
          "max_upload_bytes": {
            "type": "integer",
            "title": "Max Upload Bytes"
          },
          "retention_hours": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Retention Hours"
          },
          "processing_credits": {
            "type": "integer",
            "title": "Processing Credits"
          },
          "processing_credits_used": {
            "type": "integer",
            "title": "Processing Credits Used"
          },
          "credit_period": {
            "type": "string",
            "enum": [
              "rolling_24h",
              "calendar_month",
              "billing_month"
            ],
            "title": "Credit Period"
          },
          "credit_period_started_at": {
            "type": "string",
            "format": "date-time",
            "title": "Credit Period Started At"
          },
          "credit_period_ends_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Credit Period Ends At"
          },
          "accepted_terms_version": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Accepted Terms Version"
          },
          "accepted_privacy_version": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Accepted Privacy Version"
          },
          "display_name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Display Name"
          },
          "avatar_seed": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Avatar Seed"
          }
        },
        "type": "object",
        "required": [
          "kind",
          "id",
          "tier",
          "plan",
          "email",
          "max_upload_bytes",
          "processing_credits",
          "processing_credits_used",
          "credit_period",
          "credit_period_started_at"
        ],
        "title": "MeResponse",
        "description": "Who the caller is and what their tier allows.\n\nEvery gated control in the UI reads from this — there is no other way for the\nbrowser to learn its own tier, since the credential it holds is opaque to it."
      },
      "MeshStats": {
        "properties": {
          "triangles": {
            "type": "integer",
            "title": "Triangles"
          },
          "vertices": {
            "type": "integer",
            "title": "Vertices"
          },
          "size_bytes": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Size Bytes"
          }
        },
        "type": "object",
        "required": [
          "triangles",
          "vertices",
          "size_bytes"
        ],
        "title": "MeshStats",
        "description": "One side of a before/after comparison.\n\n``size_bytes`` is an integer, never a preformatted string: KB/MB rounding is\npresentation, and baking it into the API would freeze locale and precision\ninto the wire contract. The browser formats it."
      },
      "SimplifyConfig": {
        "properties": {
          "mode": {
            "type": "string",
            "enum": [
              "auto",
              "ratio"
            ],
            "title": "Mode",
            "description": "`auto` searches for the smallest mesh whose measured deviation from the original stays inside `fidelity_tolerance`, and is what most callers want. `ratio` applies `target_ratio` in one pass instead.",
            "default": "auto"
          },
          "target_ratio": {
            "anyOf": [
              {
                "type": "number",
                "maximum": 1,
                "exclusiveMinimum": 0
              },
              {
                "type": "null"
              }
            ],
            "title": "Target Ratio",
            "description": "Fraction of the original triangle count to keep, for `mode=\"ratio\"`. 0.25 asks for a quarter. Ignored in `auto` mode."
          },
          "preserve_appearance": {
            "type": "boolean",
            "title": "Preserve Appearance",
            "description": "Keep materials, textures, UVs and vertex colours. Turning it off produces a markedly smaller mesh, and not only because the images stop being written: UVs leave the simplifier's weld key, so seams it would otherwise treat as uncrossable borders become collapsible. On a seam-heavy asset that second effect is the larger one.",
            "default": true
          },
          "use_alpha_wrap": {
            "type": "boolean",
            "title": "Use Alpha Wrap",
            "description": "**Rebuild the surface as a watertight shell instead of decimating it.** Alpha wrapping constructs a new closed surface around the model, which repairs holes, self-intersections, loose triangles and non-manifold geometry that decimation cannot process. The trade is real: the new surface shares no vertex with the original, so materials, textures and UVs cannot be carried onto it and colour is re-sampled as vertex colours. Fine detail and openings are closed over. Use `alpha` and `offset` to control how closely it clings.",
            "default": false
          },
          "alpha": {
            "anyOf": [
              {
                "type": "number",
                "maximum": 500,
                "exclusiveMinimum": 0
              },
              {
                "type": "null"
              }
            ],
            "title": "Alpha",
            "description": "How tightly a rebuilt surface clings, relative to the model's bounding diagonal — larger is tighter. Only read when `use_alpha_wrap` is set. Cost grows roughly quadratically, which is why it is bounded.",
            "default": 75
          },
          "offset": {
            "anyOf": [
              {
                "type": "number",
                "maximum": 20000,
                "exclusiveMinimum": 0
              },
              {
                "type": "null"
              }
            ],
            "title": "Offset",
            "description": "How far outside the original surface a rebuilt one sits, relative to the bounding diagonal — larger is closer. Only read when `use_alpha_wrap` is set.",
            "default": 2000
          },
          "skip_meshoptimizer": {
            "type": "boolean",
            "title": "Skip Meshoptimizer",
            "description": "Rebuild the surface without decimating it afterwards. Produces a watertight result at close to its natural density rather than a smaller proxy. Only read when `use_alpha_wrap` is set.",
            "default": false
          },
          "fidelity_tolerance": {
            "type": "number",
            "maximum": 0.05,
            "exclusiveMinimum": 0,
            "title": "Fidelity Tolerance",
            "description": "How far `auto` will let the surface move, as a fraction of the model's bounding diagonal, averaged over points sampled across its area. It also bounds how far a texture may slide or a vertex colour shift, so staying inside it means the result still *looks* right rather than merely holding its silhouette. Three values are offered in the app: 0.005 (near-lossless, the default), 0.008 (balanced), 0.015 (aggressive). Only read in `auto` mode.",
            "default": 0.005
          },
          "auto_min_ratio": {
            "type": "number",
            "exclusiveMaximum": 1,
            "exclusiveMinimum": 0,
            "title": "Auto Min Ratio",
            "description": "The smallest fraction of the original `auto` will search down to, so the search has a floor however permissive the tolerance is.",
            "default": 0.004
          },
          "max_deviation": {
            "type": "number",
            "exclusiveMaximum": 1,
            "exclusiveMinimum": 0,
            "title": "Max Deviation",
            "description": "A safety rail rather than a quality setting: a candidate that moves the surface further than this is rejected outright, which is what stops an aggressive pass returning a mesh with components missing.",
            "default": 0.1
          },
          "fallback_to_alpha_wrap": {
            "type": "boolean",
            "title": "Fallback To Alpha Wrap",
            "description": "If ordinary decimation fails — usually geometry too broken to collapse — rebuild the surface as a watertight shell instead of failing the job. The result then carries the same trade `use_alpha_wrap` describes, and the job says so in its warnings.",
            "default": true
          },
          "output_format": {
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "obj",
                  "stl",
                  "ply",
                  "off",
                  "glb"
                ]
              },
              {
                "type": "null"
              }
            ],
            "title": "Output Format",
            "description": "What to write the result as. Null keeps the source's format. OBJ, STL, PLY and OFF cannot carry textures in a single file, so a textured source written to one loses them and the job says so."
          },
          "output_profile": {
            "type": "string",
            "enum": [
              "universal",
              "compact",
              "minimum"
            ],
            "title": "Output Profile",
            "description": "Which glTF extensions a GLB result may use, which is a compatibility promise rather than a compression level. `universal` declares none and opens anywhere; `compact` and `minimum` are progressively smaller and need a reader that supports the extensions they declare. Ignored for every non-GLB format.",
            "default": "universal"
          }
        },
        "type": "object",
        "title": "SimplifyConfig",
        "description": "Validated, immutable instructions for a ``simplify`` job.\n\n``auto`` searches progressively lower meshoptimizer target counts and keeps\nthe smallest result whose *measured* surface deviation from the original\nstays within ``fidelity_tolerance``. A ratio is deliberately applied in one\ninvocation.\n\nAuto used to stop on meshoptimizer's own ``result_error`` instead, which is\na quadric error over the attribute stream rather than a distance — and at a\nbudget a hundred times tighter than the one a ratio runs under. It stopped\nat 86% of the source on one test asset and returned a mesh both larger and\nless faithful than a plain ``target_ratio=0.25`` on another, because its\nonly acceptance test was \"fewer triangles than the last pass\"."
      },
      "ThumbnailResponse": {
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "processing",
              "ready",
              "failed"
            ],
            "title": "Status"
          },
          "url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Url"
          },
          "expires_in_seconds": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Expires In Seconds"
          },
          "error_code": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Error Code"
          }
        },
        "type": "object",
        "required": [
          "status"
        ],
        "title": "ThumbnailResponse",
        "description": "Where an asset's preview image is, if it exists yet.\n\n``url`` is null for every status but ``ready``, and that is a normal answer\nrather than an error: a freshly uploaded asset legitimately has no image for\na few seconds, and a 404 there would make the library treat \"not yet\" and\n\"never\" the same way. The key is not in this response at any status."
      },
      "UploadListResponse": {
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/UploadSummary"
            },
            "type": "array",
            "title": "Items"
          },
          "total": {
            "type": "integer",
            "title": "Total"
          }
        },
        "type": "object",
        "required": [
          "items",
          "total"
        ],
        "title": "UploadListResponse"
      },
      "UploadResponse": {
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "title": "Id"
          },
          "status": {
            "type": "string",
            "title": "Status"
          },
          "format": {
            "type": "string",
            "title": "Format"
          },
          "original_name": {
            "type": "string",
            "title": "Original Name"
          },
          "size_bytes": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Size Bytes"
          },
          "triangle_count": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Triangle Count"
          },
          "vertex_count": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Vertex Count"
          },
          "thumbnail_status": {
            "type": "string",
            "enum": [
              "pending",
              "processing",
              "ready",
              "failed"
            ],
            "title": "Thumbnail Status",
            "default": "pending"
          },
          "expires_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Expires At"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "title": "Created At"
          }
        },
        "type": "object",
        "required": [
          "id",
          "status",
          "format",
          "original_name",
          "size_bytes",
          "expires_at",
          "created_at"
        ],
        "title": "UploadResponse",
        "description": "One source asset. ``upload`` is this codebase's name for the product's *asset*.\n\nThe row was always the persistent, reusable source a job reads from; the\nredesign surfaces it as a library rather than renaming it, because the name\nis load-bearing in storage keys, the sweep, and the claim flow."
      },
      "UploadSummary": {
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "title": "Id"
          },
          "status": {
            "type": "string",
            "title": "Status"
          },
          "format": {
            "type": "string",
            "title": "Format"
          },
          "original_name": {
            "type": "string",
            "title": "Original Name"
          },
          "size_bytes": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Size Bytes"
          },
          "triangle_count": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Triangle Count"
          },
          "vertex_count": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Vertex Count"
          },
          "thumbnail_status": {
            "type": "string",
            "enum": [
              "pending",
              "processing",
              "ready",
              "failed"
            ],
            "title": "Thumbnail Status",
            "default": "pending"
          },
          "expires_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Expires At"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "title": "Created At"
          },
          "job_count": {
            "type": "integer",
            "title": "Job Count"
          },
          "latest_job_status": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Latest Job Status"
          },
          "latest_job_type": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Latest Job Type"
          },
          "thumbnail_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Thumbnail Url"
          },
          "thumbnail_expires_in_seconds": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Thumbnail Expires In Seconds"
          }
        },
        "type": "object",
        "required": [
          "id",
          "status",
          "format",
          "original_name",
          "size_bytes",
          "expires_at",
          "created_at",
          "job_count",
          "latest_job_status",
          "latest_job_type"
        ],
        "title": "UploadSummary",
        "description": "A library card: one asset plus the shape of its processing history.\n\nThe two job fields are aggregates computed alongside the listing query rather\nthan fetched per row, so a library of a hundred assets is still two queries."
      },
      "UploadUrlResponse": {
        "properties": {
          "upload_id": {
            "type": "string",
            "format": "uuid",
            "title": "Upload Id",
            "description": "The completed asset to run this against."
          },
          "url": {
            "type": "string",
            "title": "Url"
          },
          "method": {
            "type": "string",
            "const": "PUT",
            "title": "Method"
          },
          "format": {
            "type": "string",
            "title": "Format"
          },
          "max_bytes": {
            "type": "integer",
            "title": "Max Bytes"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "title": "Expires At"
          }
        },
        "type": "object",
        "required": [
          "upload_id",
          "url",
          "method",
          "format",
          "max_bytes",
          "expires_at"
        ],
        "title": "UploadUrlResponse"
      },
      "UsageResponse": {
        "properties": {
          "bytes_stored": {
            "type": "integer",
            "title": "Bytes Stored"
          },
          "bytes_output": {
            "type": "integer",
            "title": "Bytes Output"
          },
          "bytes_processed": {
            "type": "integer",
            "title": "Bytes Processed"
          },
          "bytes_processed_period": {
            "type": "integer",
            "title": "Bytes Processed Period"
          },
          "asset_count": {
            "type": "integer",
            "title": "Asset Count"
          },
          "job_count": {
            "type": "integer",
            "title": "Job Count"
          },
          "bytes_processed_limit": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Bytes Processed Limit"
          },
          "bytes_storage_limit": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Bytes Storage Limit"
          },
          "concurrent_jobs_limit": {
            "type": "integer",
            "title": "Concurrent Jobs Limit"
          },
          "processing_credits": {
            "type": "integer",
            "title": "Processing Credits"
          },
          "processing_credits_used": {
            "type": "integer",
            "title": "Processing Credits Used"
          },
          "credit_period": {
            "type": "string",
            "enum": [
              "rolling_24h",
              "calendar_month",
              "billing_month"
            ],
            "title": "Credit Period"
          },
          "credit_period_started_at": {
            "type": "string",
            "format": "date-time",
            "title": "Credit Period Started At"
          },
          "credit_period_ends_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Credit Period Ends At"
          },
          "data_period": {
            "type": "string",
            "enum": [
              "calendar_month",
              "billing_month"
            ],
            "title": "Data Period"
          },
          "data_period_started_at": {
            "type": "string",
            "format": "date-time",
            "title": "Data Period Started At"
          },
          "data_period_ends_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Data Period Ends At"
          },
          "retention_days": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Retention Days"
          },
          "retention_hours": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Retention Hours"
          }
        },
        "type": "object",
        "required": [
          "bytes_stored",
          "bytes_output",
          "bytes_processed",
          "bytes_processed_period",
          "asset_count",
          "job_count",
          "bytes_processed_limit",
          "bytes_storage_limit",
          "concurrent_jobs_limit",
          "processing_credits",
          "processing_credits_used",
          "credit_period",
          "credit_period_started_at",
          "data_period",
          "data_period_started_at",
          "retention_days"
        ],
        "title": "UsageResponse",
        "description": "What this owner has stored and processed, counted from rows, never a client.\n\n**Three meters, and they measure three different things.** Storage is a\npoint-in-time sum of retained bytes (live sources plus live outputs), and\nfalls when things expire. Processed data is the sum of *source input* bytes\nacross the jobs in the current data window, so re-crunching one asset counts\nonce per run — it measures compute consumed, not data kept. Processing\ncredits count *operations* over the credit window, which is a different\nwindow on a paid plan and bounded by a different number. A customer can be\nstopped by any one of them, so all three are reported with their own limit\nand their own reset.\n\nEvery limit here is the same number the API enforces, resolved from the plan\npolicy — never restated."
      }
    },
    "securitySchemes": {
      "ApiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "MeshCrunch API key",
        "description": "An API key, presented as `Authorization: Bearer mcrunch_live_<id>_<secret>`.\n\nKeys issued before this format read `mc_<id>_<secret>` and remain valid — the digest is taken over the whole string, so nothing was revoked by the change.\n\nCreate one in Studio under Settings → API keys. Programmatic access is part of a paid subscription and is re-checked on every request, so a key follows the subscription rather than the moment it was minted. The scope chosen at creation (`read` or `write`) is enforced per operation."
      },
      "SessionToken": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "An Auth0 access token from a signed-in browser session. Required only by the key-management operations, which are refused to API keys so that a leaked key cannot mint a successor or revoke the one being used to withdraw it."
      }
    }
  }
}