{"openapi":"3.1.0","info":{"title":"Vizbl 3D Converter","description":"\nConvert & optimize 3D / CAD meshes for web and AR.\n\n**Pipeline:** upload a model → optimize/convert → download a compressed `GLB`,\n`USDZ`, or `FBX`.\n\n**Engines** (per-request `engine` field):\n- `local` — in-process open-source pipeline (Blender / gmsh / gltfpack / gltf-transform).\n- `vntana` — remote **VNTANA Operation Engine** on AWS Batch (metered ~$2/optimization;\n  available only when configured — see `GET /api/status`).\n\n**Auth:** submitting a conversion and inspecting jobs require the `X-API-Key`\nheader. Downloading a finished output is open — the job id is the capability —\nas are `GET /api/status` and `/healthz`.\n\nKeys carry a **scope** and an optional **usage limit** (max conversions):\n- `full` — every feature, including `engine=vntana`.\n- `no-vntana` — everything except `engine=vntana` (that combination returns `403`).\n\nExceeding a key's usage limit returns `429`. Keys are issued, revoked and\nmonitored in the password-protected admin dashboard at `/admin`.\n","version":"0.1.0"},"paths":{"/api/status":{"get":{"tags":["service"],"summary":"Service status & capabilities","description":"Public, unauthenticated. Reports tool availability, which **engines** are\nusable (`local` always; `vntana` only when configured), supported source and\ntarget formats, current job counts, and upload limits.","operationId":"api_status_api_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/convert":{"post":{"tags":["conversion"],"summary":"Submit a conversion","description":"Submit a model for conversion / optimization.\n\n**Async (default):** returns a `Job` record with `202`; poll `GET /api/jobs/{id}`\nand then `GET /api/jobs/{id}/download`.\n\n**Sync (`?wait=true`):** blocks up to `wait_timeout` seconds and returns the\noptimized file directly (octet-stream) with `X-Reduction-Pct`,\n`X-Source-Triangles`, `X-Output-Triangles`, `X-Triangle-Reduction-Pct` headers.\n\nRequires the `X-API-Key` header. `engine=vntana` additionally requires a\n`full`-scope key (`403` otherwise), and every accepted conversion counts\nagainst the key's usage limit (`429` once it is used up).","operationId":"api_convert_api_convert_post","security":[{"APIKeyHeader":[]}],"parameters":[{"name":"wait","in":"query","required":false,"schema":{"type":"boolean","description":"If true, block until the job finishes and return the file directly instead of a job record.","default":false,"title":"Wait"},"description":"If true, block until the job finishes and return the file directly instead of a job record."},{"name":"wait_timeout","in":"query","required":false,"schema":{"type":"integer","maximum":3600,"minimum":1,"description":"Max seconds to block when wait=true.","default":300,"title":"Wait Timeout"},"description":"Max seconds to block when wait=true."}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_api_convert_api_convert_post"}}}},"responses":{"202":{"description":"Job accepted (async). Returns the job record.","content":{"application/json":{"schema":{}}}},"200":{"description":"With `?wait=true`: the converted file (octet-stream) plus `X-Reduction-Pct` / `X-*-Triangles` metadata headers.","content":{"application/octet-stream":{}}},"401":{"description":"Missing or invalid API key."},"403":{"description":"The presented key may not use `engine=vntana`."},"413":{"description":"File exceeds the upload size limit."},"415":{"description":"Unsupported source format."},"422":{"description":"Invalid option value."},"429":{"description":"The key's usage limit is exhausted."},"502":{"description":"`wait=true`: the conversion failed."},"504":{"description":"`wait=true`: still running when the timeout elapsed — poll the job."}}}},"/api/jobs":{"get":{"tags":["conversion"],"summary":"List recent jobs","description":"Return recent jobs (newest first), each with status and reduction stats.","operationId":"api_jobs_api_jobs_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/Job"},"type":"array","title":"Response Api Jobs Api Jobs Get"}}}},"401":{"description":"Missing or invalid API key."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"APIKeyHeader":[]}]}},"/api/jobs/{job_id}":{"get":{"tags":["conversion"],"summary":"Get a job","description":"Fetch one job's current status, options, and reduction stats.","operationId":"api_job_api_jobs__job_id__get","security":[{"APIKeyHeader":[]}],"parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Job"}}}},"401":{"description":"Missing or invalid API key."},"404":{"description":"Job not found."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/jobs/{job_id}/download":{"get":{"tags":["conversion"],"summary":"Download a job's output","description":"Download the produced asset for a finished job.\n\nOpen on purpose: the key is spent on *producing* the file, and the download\nlink has to work in a browser, in an `<a href>`, and for any service handed\nthe URL. The job id is the capability: it is unguessable and the job list\nstays behind the key, so knowing the id is what grants access.","operationId":"api_download_api_jobs__job_id__download_get","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}}],"responses":{"200":{"description":"The converted file.","content":{"application/json":{"schema":{}},"application/octet-stream":{}}},"404":{"description":"Job not found."},"409":{"description":"Output not available yet (job not done)."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/keys":{"get":{"tags":["admin"],"summary":"List API keys with their usage statistics","operationId":"list_keys_api_admin_keys_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"401":{"description":"Missing or invalid admin credentials."}},"security":[{"HTTPBasic":[]}]},"post":{"tags":["admin"],"summary":"Issue a new API key","operationId":"create_key_api_admin_keys_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyCreate"}}},"required":true},"responses":{"201":{"description":"The created key (full secret value included).","content":{"application/json":{"schema":{}}}},"401":{"description":"Missing or invalid admin credentials."},"409":{"description":"That key value already exists."},"422":{"description":"Invalid scope or usage limit."}},"security":[{"HTTPBasic":[]}]}},"/api/admin/keys/{key_id}":{"patch":{"tags":["admin"],"summary":"Update a key's scope, limit or counters","operationId":"update_key_api_admin_keys__key_id__patch","security":[{"HTTPBasic":[]}],"parameters":[{"name":"key_id","in":"path","required":true,"schema":{"type":"string","title":"Key Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"401":{"description":"Missing or invalid admin credentials."},"404":{"description":"No such key."},"422":{"description":"Invalid scope or usage limit."}}},"delete":{"tags":["admin"],"summary":"Revoke an API key","operationId":"delete_key_api_admin_keys__key_id__delete","security":[{"HTTPBasic":[]}],"parameters":[{"name":"key_id","in":"path","required":true,"schema":{"type":"string","title":"Key Id"}}],"responses":{"204":{"description":"Key deleted."},"401":{"description":"Missing or invalid admin credentials."},"404":{"description":"No such key."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"Body_api_convert_api_convert_post":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File","description":"3D/CAD model to convert (see GET /api/status for source formats)."},"target_format":{"type":"string","enum":["glb","usdz","fbx"],"title":"Target Format","description":"Output format: glb (web), usdz (iOS AR), fbx (game engine).","default":"glb"},"optimize":{"type":"boolean","title":"Optimize","description":"Run geometry simplification + texture optimization.","default":true},"simplify_ratio":{"type":"number","maximum":1.0,"exclusiveMinimum":0.0,"title":"Simplify Ratio","description":"Fraction of triangles to keep (0–1]. 1.0 = no simplification.","default":0.5},"texture_size":{"type":"integer","maximum":8192.0,"minimum":64.0,"title":"Texture Size","description":"Max texture size in pixels.","default":2048},"texture_quality":{"anyOf":[{"type":"integer","maximum":10.0,"minimum":1.0},{"type":"null"}],"title":"Texture Quality","description":"Color-texture encoding quality 1–10 (KTX2/ETC1S, glb target). Unset = server default."},"compression":{"type":"string","enum":["meshopt","draco","none"],"title":"Compression","description":"glTF geometry compression scheme.","default":"meshopt"},"engine":{"type":"string","enum":["local","vntana"],"title":"Engine","description":"Conversion backend. `local` = in-process open-source pipeline; `vntana` = remote VNTANA Operation Engine on AWS Batch (metered ~$2/optimization). Availability is reported by GET /api/status.","default":"local"}},"type":"object","required":["file"],"title":"Body_api_convert_api_convert_post"},"ConversionOptions":{"properties":{"target_format":{"type":"string","title":"Target Format","description":"Output format: glb (web) | usdz (iOS AR) | fbx (game engine).","default":"glb"},"optimize":{"type":"boolean","title":"Optimize","description":"Run geometry simplification + texture optimization.","default":true},"simplify_ratio":{"type":"number","maximum":1.0,"exclusiveMinimum":0.0,"title":"Simplify Ratio","description":"Fraction of triangles to keep (0–1]. 1.0 = no simplification.","default":0.5},"texture_size":{"type":"integer","maximum":8192.0,"minimum":64.0,"title":"Texture Size","description":"Max texture size in pixels.","default":2048},"texture_quality":{"anyOf":[{"type":"integer","maximum":10.0,"minimum":1.0},{"type":"null"}],"title":"Texture Quality","description":"Color-texture encoding quality 1–10 (KTX2/ETC1S, glb target). Unset = server default."},"compression":{"type":"string","title":"Compression","description":"glTF geometry compression: meshopt | draco | none.","default":"meshopt"},"engine":{"type":"string","title":"Engine","description":"Backend: local (in-process open-source) | vntana (remote AWS Batch, metered).","default":"local"}},"type":"object","title":"ConversionOptions","description":"Per-request conversion options."},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"Job":{"properties":{"id":{"type":"string","title":"Id","description":"Job id."},"status":{"$ref":"#/components/schemas/JobStatus","description":"Lifecycle: queued | running | done | error.","default":"queued"},"options":{"$ref":"#/components/schemas/ConversionOptions"},"api_key_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key Id","description":"Id of the API key that submitted the job (admin audit)."},"input_filename":{"type":"string","title":"Input Filename","description":"Original uploaded filename."},"input_format":{"type":"string","title":"Input Format","description":"Detected source extension."},"input_bytes":{"type":"integer","title":"Input Bytes","description":"Input size in bytes.","default":0},"output_filename":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Output Filename","description":"Produced filename (when done)."},"output_format":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Output Format","description":"Produced format (when done)."},"output_bytes":{"type":"integer","title":"Output Bytes","description":"Output size in bytes.","default":0},"reduction_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Reduction Pct","description":"Byte-size reduction percent (negative if larger)."},"source_triangles":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Source Triangles","description":"Triangle count of the source (glTF inputs only)."},"output_triangles":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Output Triangles","description":"Triangle count of the output (glb only)."},"triangle_reduction_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Triangle Reduction Pct","description":"Triangle-count reduction percent."},"steps":{"items":{"$ref":"#/components/schemas/StepResult"},"type":"array","title":"Steps","description":"Per-step execution log."},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error","description":"Error detail when status is error."},"created_at":{"type":"string","format":"date-time","title":"Created At","description":"Job creation time (UTC)."},"finished_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Finished At","description":"Completion time (UTC)."}},"type":"object","required":["id","options","input_filename","input_format","created_at"],"title":"Job","description":"A conversion job and its result/stats."},"JobStatus":{"type":"string","enum":["queued","running","done","error"],"title":"JobStatus"},"KeyCreate":{"properties":{"name":{"type":"string","maxLength":80,"title":"Name","description":"Human label shown in the dashboard.","default":""},"scope":{"type":"string","enum":["full","no-vntana"],"title":"Scope","description":"full = VNTANA engine allowed; no-vntana = everything else.","default":"no-vntana"},"usage_limit":{"anyOf":[{"type":"integer","minimum":1.0},{"type":"null"}],"title":"Usage Limit","description":"Max conversions; omit or null for unlimited."},"key":{"anyOf":[{"type":"string","maxLength":200,"minLength":8},{"type":"null"}],"title":"Key","description":"Explicit key value; generated when omitted."}},"type":"object","title":"KeyCreate"},"KeyUpdate":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Name"},"scope":{"anyOf":[{"type":"string","enum":["full","no-vntana"]},{"type":"null"}],"title":"Scope"},"usage_limit":{"anyOf":[{"type":"integer","minimum":1.0},{"type":"null"}],"title":"Usage Limit","description":"null = unlimited. Omit to leave unchanged."},"reset_usage":{"type":"boolean","title":"Reset Usage","description":"Zero the usage counters for this key.","default":false}},"type":"object","title":"KeyUpdate"},"StepResult":{"properties":{"name":{"type":"string","title":"Name","description":"Step name, e.g. ingest / optimize / export / vntana-submit."},"command":{"items":{"type":"string"},"type":"array","title":"Command","description":"The command (or remote action) that was run."},"ok":{"type":"boolean","title":"Ok","description":"Whether the step succeeded."},"detail":{"type":"string","title":"Detail","description":"Tail of stdout/stderr, or a short status detail.","default":""}},"type":"object","required":["name","command","ok"],"title":"StepResult","description":"One executed step of a job's pipeline."},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"}},"securitySchemes":{"APIKeyHeader":{"type":"apiKey","in":"header","name":"X-API-Key"},"HTTPBasic":{"type":"http","scheme":"basic"}}},"tags":[{"name":"conversion","description":"Submit conversions and fetch job results."},{"name":"service","description":"Health and capability information."}]}