{"openapi":"3.1.0","info":{"title":"Splat3D API","version":"1.7.0","description":"Turn photos and videos into interactive 3D Gaussian Splat scenes."},"servers":[{"url":"https://api.splat-3d.com","description":"Production"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"API key in format: s3d_..."}},"schemas":{"SceneCompletedEvent":{"type":"object","properties":{"event":{"type":"string","enum":["scene.completed"]},"scene_id":{"type":"string","example":"a1b2c3d4e5f6"},"status":{"type":"string","enum":["complete"],"description":"The scene's status."},"timestamp":{"type":"string","format":"date-time","description":"When the event was made, UTC with milliseconds. Retries carry the same value.","example":"2026-04-13T12:05:00.000Z"},"data":{"type":"object","properties":{"viewer_url":{"type":"string","description":"The scene's viewer page on the web app.","example":"https://splat-3d.com/tour/a1b2c3d4e5f6"},"num_gaussians":{"type":["integer","null"],"description":"Gaussians in the trained scene.","example":1940000},"psnr":{"type":["number","null"],"description":"PSNR on the training views, in dB.","example":24.28},"ssim":{"type":["number","null"],"description":"SSIM on the training views.","example":0.8123},"training_time_seconds":{"type":["number","null"],"description":"Seconds the training stage took, not the whole job.","example":233.7}},"required":["viewer_url","num_gaussians","psnr","ssim","training_time_seconds"],"description":"A metric the pipeline did not report, or reported as zero, is null."}},"required":["event","scene_id","status","timestamp","data"]},"SceneFailedEvent":{"type":"object","properties":{"event":{"type":"string","enum":["scene.failed"]},"scene_id":{"type":"string","example":"a1b2c3d4e5f6"},"status":{"type":"string","enum":["failed"],"description":"The scene's status."},"timestamp":{"type":"string","format":"date-time","description":"When the event was made, UTC with milliseconds. Retries carry the same value.","example":"2026-04-13T12:05:00.000Z"},"data":{"type":"object","properties":{"error":{"type":"string","description":"Why it failed: the scene's `processing_error`.","example":"Processing failed."}},"required":["error"]}},"required":["event","scene_id","status","timestamp","data"]},"CreateSceneResponse":{"type":"object","properties":{"data":{"type":"object","properties":{"scene_id":{"type":"string","description":"The new scene.","example":"a1b2c3d4e5f6"},"id":{"type":"string","description":"Same as `scene_id`.","example":"a1b2c3d4e5f6"},"upload_url":{"type":"string","format":"uri","description":"Presigned URL: `PUT` the source file here, then call `/process`.","example":"https://r2.dev/upload?..."},"sceneId":{"type":"string","description":"Same as `scene_id`. Kept for older clients.","example":"a1b2c3d4e5f6","deprecated":true},"uploadUrl":{"type":"string","format":"uri","description":"Same as `upload_url`. Kept for older clients.","example":"https://r2.dev/upload?...","deprecated":true}},"required":["scene_id","id","upload_url","sceneId","uploadUrl"]},"meta":{"type":"object","properties":{"request_id":{"type":"string","format":"uuid"}},"required":["request_id"]}},"required":["data","meta"]},"ErrorResponse":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Machine-readable error code. One of: `invalid_input`, `source_missing`, `unauthorized`, `insufficient_credits`, `forbidden`, `insufficient_scope`, `not_found`, `conflict`, `retry_blocked`, `payload_too_large`, `source_unusable`, `quota_exceeded`, `queue_full`, `rate_limited`, `internal_error`, `upstream_error`, `service_unavailable`. More may be added, so treat an unknown code by its HTTP status.","example":"not_found"},"message":{"type":"string","example":"Scene not found."},"details":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Structured detail, absent on most errors. A request that fails validation (400 `invalid_input`) lists one `{field, message}` per problem. A parameter outside the tier's range (422) lists `{field, value, allowed, tier, hint}`; `source_unusable` names the rule that failed.","example":[{"field":"iterations","value":15000,"allowed":{"min":3000,"max":10000},"tier":"standard","hint":"iterations=15000 requires the quality tier or higher."}]}},"required":["code","message"]},"meta":{"type":"object","properties":{"request_id":{"type":"string","format":"uuid","example":"550e8400-e29b-41d4-a716-446655440000"},"period":{"type":"string","description":"`quota_exceeded`: the month whose quota is used up.","example":"2026-10"},"limit":{"type":"integer","description":"`quota_exceeded`: the monthly limit. `queue_full`: how many scenes may process at once. `rate_limited`: the requests a minute the limit that refused allows.","example":100},"field":{"type":"string","description":"`quota_exceeded`: the quota used up, `scenes_created` or `scenes_processed`.","example":"scenes_created"},"current":{"type":"integer","description":"`queue_full`: how many of your scenes are processing now.","example":2}},"required":["request_id"]}},"required":["error","meta"]},"CreateSceneBody":{"type":"object","properties":{"title":{"type":["string","null"],"maxLength":200,"example":"My scene"},"address":{"type":["string","null"],"maxLength":500,"example":"123 Main St"},"content_type":{"type":"string","example":"video/mp4"},"file_size":{"type":"integer","minimum":0,"example":52428800},"preset":{"type":"string","enum":["fast","standard","quality","ultra","custom"],"example":"standard"},"training_model":{"type":"string","enum":["gsplat"],"example":"gsplat"},"training_tier":{"type":"string","enum":["fast","standard","quality","pro","hd","ultra"],"description":"Legacy alias of `preset`, read only when `preset` is absent. `pro` and `hd` mean `quality`."},"sfm":{"type":"object","additionalProperties":{}},"training":{"type":"object","additionalProperties":{}},"output":{"type":"object","additionalProperties":{}},"enable_lod":{"type":"boolean","description":"Checked against the tier, not kept. Send `enable_lod` in the `/process` body to get level-of-detail chunks."},"webhook_url":{"type":"string","maxLength":2048,"format":"uri","pattern":"^[Hh][Tt][Tt][Pp][Ss]:\\/\\/","description":"https URL to receive a webhook when scene processing completes or fails.","example":"https://example.com/webhooks/splat"},"webhook_secret":{"type":"string","maxLength":256,"description":"Secret for HMAC-SHA256 webhook signature verification."}},"additionalProperties":false},"Scene":{"type":"object","properties":{"id":{"type":"string","example":"a1b2c3d4e5f6"},"title":{"type":["string","null"],"example":"My living room"},"description":{"type":["string","null"],"example":"Morning light, north window"},"address":{"type":["string","null"],"example":"123 Main St"},"status":{"type":"string","description":"The stored status, the same on every call. `uploading`, then the pipeline stages (`extracting_frames`, `estimating_poses`, `training`, `exporting`, `compressing`; older scenes may show `running_sfm` or a retired `preview_extracting`, `preview_generating` or `preview_compressing`), then `complete`, `failed` or `cancelled`. More stages may be added.","example":"complete"},"training_model":{"type":["string","null"],"example":"gsplat"},"preset":{"type":["string","null"],"enum":["fast","standard","quality","ultra","custom",null],"description":"The preset the scene was created or retrained with, from its stored parameters. Populated from creation on; null on a scene whose stored parameters name none.","example":"standard"},"is_public":{"type":"boolean","example":false},"thumbnail_r2_key":{"type":["string","null"]},"thumbnail_url":{"type":["string","null"],"description":"`GET /v1/scenes/{id}/thumbnail` when the scene has a thumbnail. A private scene's needs its owner's API key."},"num_gaussians":{"type":["number","null"]},"ssim":{"type":["number","null"],"description":"Mean SSIM of the trained model over its training images, so in-sample, as `psnr` is. Populated when complete."},"psnr":{"type":["number","null"],"description":"Mean PSNR in dB of the trained model over its training images. In-sample, so usually higher than `psnr_holdout`. Set when the scene completes, and kept if it later fails.","example":26.1},"psnr_holdout":{"type":["number","null"],"description":"Holdout PSNR — populated when complete if images were held out (`output.holdout_ratio` above 0; every preset but `fast` by default)."},"ssim_holdout":{"type":["number","null"],"description":"Holdout SSIM — populated when complete if images were held out (`output.holdout_ratio` above 0; every preset but `fast` by default)."},"training_time_seconds":{"type":["number","null"],"description":"Seconds the GPU training stage took, as the pipeline measured it. Frame extraction, pose estimation, compression and queueing are outside it: `processing_started_at` to `processing_completed_at` spans the whole job. Set when the scene completes, and kept if it later fails.","example":233.7},"spz_size_bytes":{"type":["integer","null"],"description":"Size in bytes of the scene's SPZ file. Populated when complete and the scene has one; null otherwise.","example":21000000},"sog_size_bytes":{"type":["integer","null"],"description":"Size in bytes of the scene's SOG file. Populated when complete and the scene has one; null otherwise.","example":48000000},"ply_size_bytes":{"type":["integer","null"],"description":"Size in bytes of the scene's PLY file. Populated when complete and the scene has one; null otherwise.","example":460000000},"processing_stage":{"type":["string","null"],"description":"Current pipeline stage while processing; null otherwise, including once complete."},"processing_pct":{"type":["number","null"],"description":"0-100 progress while processing; null otherwise, including once complete."},"processing_started_at":{"type":["string","null"],"format":"date-time","description":"When processing was launched. Null until then. Kept after the scene finishes, fails or is cancelled.","example":"2026-04-13T12:01:00.000Z"},"processing_completed_at":{"type":["string","null"],"format":"date-time","description":"When the scene was recorded complete: usually the job's end, though it can trail it by minutes. Populated when complete; null otherwise.","example":"2026-04-13T12:10:46.000Z"},"created_at":{"type":"string","format":"date-time","example":"2026-04-13T12:00:00.000Z"},"updated_at":{"type":"string","format":"date-time","example":"2026-04-13T12:05:00.000Z"}},"required":["id","title","description","address","status","training_model","preset","is_public","thumbnail_r2_key","thumbnail_url","num_gaussians","ssim","psnr","psnr_holdout","ssim_holdout","training_time_seconds","spz_size_bytes","sog_size_bytes","ply_size_bytes","processing_stage","processing_pct","processing_started_at","processing_completed_at","created_at","updated_at"]},"SceneDetail":{"allOf":[{"$ref":"#/components/schemas/Scene"},{"type":"object","properties":{"processing_error":{"type":["string","null"],"description":"Error message if processing failed or was cancelled."},"viewer_url":{"type":["string","null"],"description":"Shareable viewer page on the web app — populated when complete."},"download_url":{"type":["string","null"],"description":"Download URL for the output model — populated when complete."},"format":{"type":["string","null"],"enum":["sog","ply","spz",null],"description":"Format of the file `download_url` sends: `spz` if the scene has one, otherwise `sog`, otherwise `ply`. The download's `X-Splat-Format` header names the file actually sent: a scene whose SPZ is missing from storage reports `spz` and sends SOG. Populated when complete."},"lod_meta_url":{"type":["string","null"],"description":"LOD metadata URL — populated when complete with LOD enabled."}},"required":["processing_error","viewer_url","download_url","format","lod_meta_url"]}]},"PaginationMeta":{"type":"object","properties":{"request_id":{"type":"string","format":"uuid"},"next_cursor":{"type":["string","null"],"description":"Opaque. Pass it back unchanged as `cursor` for the next page. Null on the last page.","example":"MjAyNi0wNC0xM1QxMjowMDowMC4xMjM0NTYrMDA6MDA"},"has_more":{"type":"boolean"},"count":{"type":"integer"}},"required":["request_id","next_cursor","has_more","count"]},"SceneListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/SceneDetail"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}},"required":["data","meta"]},"SceneDetailResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SceneDetail"},"meta":{"type":"object","properties":{"request_id":{"type":"string","format":"uuid"}},"required":["request_id"]}},"required":["data","meta"]},"UpdateSceneBody":{"type":"object","properties":{"title":{"type":"string","maxLength":200},"address":{"type":"string","maxLength":500},"description":{"type":"string","maxLength":2000},"is_public":{"type":"boolean"}},"additionalProperties":false},"ProcessSceneResponse":{"type":"object","properties":{"data":{"type":"object","properties":{"status":{"type":"string","description":"Always `processing`: the launch was accepted. Poll `GET /v1/scenes/{id}` for the stored status.","example":"processing"},"scene_id":{"type":"string","description":"The scene launched.","example":"a1b2c3d4e5f6"},"id":{"type":"string","description":"Same as `scene_id`.","example":"a1b2c3d4e5f6"},"sceneId":{"type":"string","description":"Same as `scene_id`. Kept for older clients.","example":"a1b2c3d4e5f6","deprecated":true},"message":{"type":"string"}},"required":["status","scene_id","id","sceneId","message"]},"meta":{"type":"object","properties":{"request_id":{"type":"string","format":"uuid"}},"required":["request_id"]}},"required":["data","meta"]},"ArkitPose":{"type":"object","properties":{"timestamp":{"type":"number","description":"Capture timestamp in seconds","example":1.234},"file_path":{"type":"string","description":"Path of the image in the uploaded zip; the file name alone works when it is unique","example":"IMG_0001.jpg"},"transform":{"type":"array","items":{"type":"number"},"description":"4x4 camera-to-world matrix, column-major (ARKit simd_float4x4 layout), in a gravity-aligned world with +Y up (ARKit worldAlignment .gravity or .gravityAndHeading). Scene orientation is taken from it.","example":[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]},"intrinsics":{"type":"array","items":{"type":"number"},"description":"3x3 camera intrinsics matrix, row-major [fx,0,cx, 0,fy,cy, 0,0,1]","example":[1674,0,960,0,1674,720,0,0,1]},"width":{"type":"integer","exclusiveMinimum":0,"example":1920},"height":{"type":"integer","exclusiveMinimum":0,"example":1440}},"required":["timestamp","file_path","transform","intrinsics","width","height"]},"ProcessSceneBody":{"type":"object","properties":{"enable_lod":{"type":"boolean","example":false},"arkit_poses":{"type":"array","items":{"$ref":"#/components/schemas/ArkitPose"},"minItems":5,"maxItems":1000,"description":"Camera poses from an ARKit session. They don't skip SfM: GLOMAP still solves the camera poses from the images. The poses set the scene's up direction from ARKit gravity (at least 30 frames or photos must pair with a pose) and, for a video, which frames are extracted: up to 150, at the pose timestamps."},"lidar_points":{"type":"array","items":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":6},"maxItems":50000,"description":"LiDAR point cloud from ARKit ARMeshAnchor. Each entry is [x, y, z] or [x, y, z, r, g, b]. Max 50K points."}},"additionalProperties":false},"RetrainSceneResponse":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string","description":"The new scene, a version of the source. Poll it.","example":"f6e5d4c3b2a1"},"scene_id":{"type":"string","description":"Same as `id`.","example":"f6e5d4c3b2a1"},"source_scene_id":{"type":"string","description":"The scene retrained, left as it was.","example":"a1b2c3d4e5f6"},"retraining":{"type":"boolean"},"quality_tier":{"type":"string","description":"The tier the new scene runs at, in pricing's names.","example":"pro"}},"required":["id","scene_id","source_scene_id","retraining","quality_tier"]},"meta":{"type":"object","properties":{"request_id":{"type":"string","format":"uuid"}},"required":["request_id"]}},"required":["data","meta"]},"RetrainSceneBody":{"type":"object","properties":{"quality_tier":{"type":"string","enum":["fast","standard","pro","ultra"],"description":"The tier to run at, in pricing's names (`pro` runs the `quality` preset). Standard when neither this nor `preset` is sent.","example":"pro"},"preset":{"type":"string","enum":["fast","standard","quality","ultra"],"description":"The tier to run at, in create's preset names. Sent with `quality_tier`, both must name the same tier.","example":"quality"}},"additionalProperties":false},"CancelSceneResponse":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"cancelled":{"type":"boolean"},"refunded":{"type":"boolean","description":"True when the cancel returned credits: everything the launch charged when it came before the job started or within 10 seconds after, and later only an AI Enhance charge reserved at upload."},"refunded_credits":{"type":"integer","description":"Credits this cancel returned; 0 when it was not refunded.","example":20}},"required":["id","cancelled","refunded","refunded_credits"]},"meta":{"type":"object","properties":{"request_id":{"type":"string","format":"uuid"}},"required":["request_id"]}},"required":["data","meta"]},"UsageData":{"type":"object","properties":{"period":{"type":"string","example":"2026-04"},"scenes_created":{"type":"integer","example":3},"scenes_processed":{"type":"integer","example":2},"gpu_seconds_used":{"type":"integer","description":"Deprecated: never counted, so it reads 0.","example":0,"deprecated":true},"storage_bytes":{"type":"integer","description":"Deprecated: never counted, so it reads 0.","example":0,"deprecated":true},"limits":{"type":"object","properties":{"scenes_created":{"type":["integer","null"],"description":"Max scenes creatable this period. `null` means unlimited.","example":100},"scenes_processed":{"type":["integer","null"],"description":"Max scenes processable this period. `null` means unlimited.","example":100},"gpu_seconds":{"type":"integer","description":"Deprecated: not enforced.","example":36000,"deprecated":true},"storage_bytes":{"type":"integer","description":"Deprecated: not enforced.","example":53687091200,"deprecated":true}},"required":["scenes_created","scenes_processed","gpu_seconds","storage_bytes"]}},"required":["period","scenes_created","scenes_processed","gpu_seconds_used","storage_bytes","limits"]},"UsageResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/UsageData"},"meta":{"type":"object","properties":{"request_id":{"type":"string","format":"uuid"}},"required":["request_id"]}},"required":["data","meta"]}},"parameters":{}},"paths":{"/v1/scenes":{"post":{"tags":["Scenes"],"summary":"Create a scene","description":"Create a new scene and get a presigned upload URL for the source file.","operationId":"createScene","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSceneBody"}}}},"responses":{"201":{"description":"Scene created with presigned upload URL.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSceneResponse"}}}},"400":{"description":"`invalid_input`: a path, query, header or body value is invalid. When it fails the schema, `error.details` lists one `{field, message}` per problem and `error.message` joins them.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Authentication required.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`insufficient_scope`: the API key lacks the `write:scenes` scope, which keys created as Write or All carry. The request was refused before it read or changed any scene, quota or credit balance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"415":{"description":"The request has a body that isn't sent as `Content-Type: application/json`. `curl -d` sends form encoding unless you add that header. Nothing was created, claimed or charged.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Parameter out of range for the chosen tier. `error.details` lists each violation as {field, value, allowed, tier, hint}.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"`quota_exceeded`: the monthly scene creation quota is used up. `meta` holds `period`, `limit` and `field`. `rate_limited`: more than 300 requests, or 30 create, `/process` and `/retrain` requests, this minute, counted per user. The request was refused before it read or changed any scene, quota or credit balance. Wait `Retry-After` seconds, then send it again. `meta.limit` and `X-RateLimit-Limit` name the limit that refused.","headers":{"Retry-After":{"description":"Seconds to wait before sending the request again.","schema":{"type":"integer","example":60}},"X-RateLimit-Limit":{"description":"Requests a minute the limit that refused allows. Every `/v1` response carries it, naming its route's limit.","schema":{"type":"integer","example":300}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"`internal_error`: an unexpected failure on our side. Contact support with `meta.request_id`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"Temporarily unavailable, e.g. a database blip. Not an answer about your key or scene. No `Retry-After`: this operation doesn't dedupe repeats, and a repeated create or `/retrain` makes another scene. Check `GET /v1/scenes` before sending it again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"get":{"tags":["Scenes"],"summary":"List scenes","description":"List all scenes for the authenticated user, newest first, with cursor-based pagination. Each item has the shape `GET /v1/scenes/{id}` returns.","operationId":"listScenes","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","description":"`meta.next_cursor` from the previous page, unchanged. Opaque.","example":"MjAyNi0wNC0xM1QxMjowMDowMC4xMjM0NTYrMDA6MDA"},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"example":50},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Paginated list of scenes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SceneListResponse"}}}},"400":{"description":"`invalid_input`: a path, query, header or body value is invalid. When it fails the schema, `error.details` lists one `{field, message}` per problem and `error.message` joins them.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Authentication required.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`insufficient_scope`: the API key lacks the `read:scenes` scope, which keys created as Read, Write or All carry. The request was refused before it read or changed any scene, quota or credit balance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"`rate_limited`: more than 300 requests this minute, counted per user, or per client IP without a key. The request was refused before it read or changed any scene, quota or credit balance. Wait `Retry-After` seconds, then send it again. `meta.limit` and `X-RateLimit-Limit` name the limit that refused.","headers":{"Retry-After":{"description":"Seconds to wait before sending the request again.","schema":{"type":"integer","example":60}},"X-RateLimit-Limit":{"description":"Requests a minute the limit that refused allows. Every `/v1` response carries it, naming its route's limit.","schema":{"type":"integer","example":300}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"`internal_error`: an unexpected failure on our side. Contact support with `meta.request_id`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"Temporarily unavailable, e.g. a database blip. Not an answer about your key or scene: retry the same request after `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying the same request.","schema":{"type":"integer","example":5}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/scenes/{id}":{"get":{"tags":["Scenes"],"summary":"Get scene status","description":"Get detailed status and metadata for a scene. `status` is the stored status, as the list and PATCH return it. While the scene processes, `processing_stage` and `processing_pct` carry live progress.","operationId":"getScene","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","description":"Scene ID","example":"a1b2c3d4e5f6"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Scene detail — consistent shape for all processing states.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SceneDetailResponse"}}}},"400":{"description":"`invalid_input`: a path, query, header or body value is invalid. When it fails the schema, `error.details` lists one `{field, message}` per problem and `error.message` joins them.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Authentication required.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`insufficient_scope`: the API key lacks the `read:scenes` scope, which keys created as Read, Write or All carry. The request was refused before it read or changed any scene, quota or credit balance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Scene not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"`rate_limited`: more than 300 requests this minute, counted per user, or per client IP without a key. The request was refused before it read or changed any scene, quota or credit balance. Wait `Retry-After` seconds, then send it again. `meta.limit` and `X-RateLimit-Limit` name the limit that refused.","headers":{"Retry-After":{"description":"Seconds to wait before sending the request again.","schema":{"type":"integer","example":60}},"X-RateLimit-Limit":{"description":"Requests a minute the limit that refused allows. Every `/v1` response carries it, naming its route's limit.","schema":{"type":"integer","example":300}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"`internal_error`: an unexpected failure on our side. Contact support with `meta.request_id`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"Temporarily unavailable, e.g. a database blip. Not an answer about your key or scene: retry the same request after `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying the same request.","schema":{"type":"integer","example":5}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"patch":{"tags":["Scenes"],"summary":"Update scene metadata","description":"Update title, address, description, or visibility of a scene.","operationId":"updateScene","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","description":"Scene ID","example":"a1b2c3d4e5f6"},"required":true,"name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSceneBody"}}}},"responses":{"200":{"description":"Updated scene, in the same shape as GET /v1/scenes/{id}.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SceneDetailResponse"}}}},"400":{"description":"`invalid_input`: a path, query, header or body value is invalid. When it fails the schema, `error.details` lists one `{field, message}` per problem and `error.message` joins them.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Authentication required.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`insufficient_scope`: the API key lacks the `write:scenes` scope, which keys created as Write or All carry. The request was refused before it read or changed any scene, quota or credit balance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Scene not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"`rate_limited`: more than 300 requests this minute, counted per user, or per client IP without a key. The request was refused before it read or changed any scene, quota or credit balance. Wait `Retry-After` seconds, then send it again. `meta.limit` and `X-RateLimit-Limit` name the limit that refused.","headers":{"Retry-After":{"description":"Seconds to wait before sending the request again.","schema":{"type":"integer","example":60}},"X-RateLimit-Limit":{"description":"Requests a minute the limit that refused allows. Every `/v1` response carries it, naming its route's limit.","schema":{"type":"integer","example":300}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"`internal_error`: an unexpected failure on our side. Contact support with `meta.request_id`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"Temporarily unavailable, e.g. a database blip. Not an answer about your key or scene. No `Retry-After`: this operation doesn't dedupe repeats, and a repeated create or `/retrain` makes another scene. Check `GET /v1/scenes` before sending it again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"tags":["Scenes"],"summary":"Delete a scene","description":"Delete a scene. Every call answers `404` for it at once; a daily cleanup removes its files 24 to 48 hours later. An unfinished scene is cancelled first, under the cancel refund rules; if the cancel fails, nothing is deleted.","operationId":"deleteScene","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","description":"Scene ID","example":"a1b2c3d4e5f6"},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"Scene deleted."},"400":{"description":"`invalid_input`: a path, query, header or body value is invalid. When it fails the schema, `error.details` lists one `{field, message}` per problem and `error.message` joins them.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Authentication required.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`insufficient_scope`: the API key lacks the `write:scenes` scope, which keys created as Write or All carry. The request was refused before it read or changed any scene, quota or credit balance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Scene not found: no such scene, already deleted, or owned by another account. These are not told apart.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The scene changed while it was being deleted. Try again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"`rate_limited`: more than 300 requests this minute, counted per user, or per client IP without a key. The request was refused before it read or changed any scene, quota or credit balance. Wait `Retry-After` seconds, then send it again. `meta.limit` and `X-RateLimit-Limit` name the limit that refused.","headers":{"Retry-After":{"description":"Seconds to wait before sending the request again.","schema":{"type":"integer","example":60}},"X-RateLimit-Limit":{"description":"Requests a minute the limit that refused allows. Every `/v1` response carries it, naming its route's limit.","schema":{"type":"integer","example":300}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"`internal_error`: an unexpected failure on our side. Contact support with `meta.request_id`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"Temporarily unavailable, e.g. a database blip. Not an answer about your key or scene: retry the same request after `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying the same request.","schema":{"type":"integer","example":5}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/scenes/{id}/process":{"post":{"tags":["Scenes"],"summary":"Start processing a scene","description":"Reserve one paid launch for an uploaded scene. Retry with the same body and optional Idempotency-Key to replay the saved result without another charge, processing-quota increment, or dispatch. An uncertain upstream response remains accepted until status reconciliation resolves it.","operationId":"processScene","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","description":"Scene ID","example":"a1b2c3d4e5f6"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","pattern":"^[\\x21-\\x7e]{1,128}$","description":"Optional user-scoped key. Reuse this exact key and body on retries. A different key/body for the same scene, or reuse on another scene, returns 409."},"required":false,"name":"idempotency-key","in":"header"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProcessSceneBody"}}}},"responses":{"200":{"description":"Processing started.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProcessSceneResponse"}}}},"400":{"description":"`source_missing`: the scene's source file isn't in storage, e.g. the upload never finished. Nothing was charged. Upload it again (the upload URL from create lasts 24 hours; after that, create a new scene), then call `/process` again. Any other bad request is `invalid_input`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Authentication required.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"402":{"description":"`insufficient_credits`: your balance doesn't cover this launch. Nothing was charged. After `/process` the scene stays `uploading`: add credits and call `/process` again. After `/retrain` no new version is kept.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`insufficient_scope`: the API key lacks the `write:scenes` scope, which keys created as Write or All carry. The request was refused before it read or changed any scene, quota or credit balance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Scene not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Scene is already being processed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"413":{"description":"`payload_too_large`: the uploaded file exceeds the 5.0 GB upload limit, the same on every plan. Nothing was charged.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"415":{"description":"The request has a body that isn't sent as `Content-Type: application/json`. `curl -d` sends form encoding unless you add that header. Nothing was created, claimed or charged.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"`source_unusable`: the uploaded file won't make a usable scene: it is empty, a video under 30 seconds (read from its MP4 or MOV header; a length that can't be read launches), a 360° video at any length (a 2:1 frame, or spherical metadata on another shape; 360° video isn't supported yet), a photo upload that isn't a zip, or a zip with fewer than 20 JPEG or PNG photos (HEIC, WebP and other formats are skipped). A zip holding a COLMAP model needs 5; a SplatKing video export (one `wide.mov`, under 5 photos) isn't counted. `details` names the rule, e.g. `{ field: \"photo_count\", value: 12, allowed: { min: 20 } }`, `{ field: \"video_seconds\", value: 12.4, allowed: { min: 30 } }` or `{ field: \"video_aspect\", value: 2, allowed: { not: 2 } }` (`video_projection` for spherical metadata). Nothing was charged. Upload a usable file to the same upload URL (it lasts 24 hours from create) and call `/process` again, or create a new scene.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"`queue_full`: as many of your scenes are processing as your plan allows at once. Nothing was charged, and `meta` holds `limit` and `current`. After `/process` the scene stays `uploading`: call `/process` again once one finishes. After `/retrain` no new version is kept, unless the response carries no `Retry-After`: then check `GET /v1/scenes` before sending it again. `quota_exceeded`: the monthly quota is used up; `meta` holds `period`, `limit` and `field`. After `/retrain` no new version is kept. `rate_limited`: more than 300 requests, or 30 create, `/process` and `/retrain` requests, this minute, counted per user. The request was refused before it read or changed any scene, quota or credit balance. Wait `Retry-After` seconds, then send it again. `meta.limit` and `X-RateLimit-Limit` name the limit that refused.","headers":{"Retry-After":{"description":"Seconds to wait before sending the request again: 120 with `queue_full`, 60 with `rate_limited`. Not sent with `quota_exceeded`.","schema":{"type":"integer","example":120}},"X-RateLimit-Limit":{"description":"Requests a minute the limit that refused allows. Every `/v1` response carries it, naming its route's limit.","schema":{"type":"integer","example":300}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"`internal_error`: an unexpected failure on our side. Contact support with `meta.request_id`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"502":{"description":"`upstream_error`: the GPU backend refused the launch, or the capture data could not be stored. The charge was refunded. Repeating the same `/process` request returns this saved failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"Temporarily unavailable, e.g. a database blip. Not an answer about your key or scene: retry the same request after `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying the same request.","schema":{"type":"integer","example":5}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/scenes/{id}/retrain":{"post":{"tags":["Scenes"],"summary":"Retrain a scene","description":"Process a scene's source again as a new scene (a version), at the tier `quality_tier` or `preset` names, standard when neither is sent. `id` and `scene_id` name the new scene; the source scene is left as it is, running or not. Each request makes another version. A retrain refused before any charge keeps no new version; after a `502` the new version stays, failed and refunded.","operationId":"retrainScene","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","description":"Scene ID","example":"a1b2c3d4e5f6"},"required":true,"name":"id","in":"path"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RetrainSceneBody"}}}},"responses":{"200":{"description":"Retraining started, as a new scene.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RetrainSceneResponse"}}}},"400":{"description":"`invalid_input`: a path, query, header or body value is invalid. When it fails the schema, `error.details` lists one `{field, message}` per problem and `error.message` joins them.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Authentication required.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"402":{"description":"`insufficient_credits`: your balance doesn't cover this launch. Nothing was charged. After `/process` the scene stays `uploading`: add credits and call `/process` again. After `/retrain` no new version is kept.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`insufficient_scope`: the API key lacks the `write:scenes` scope, which keys created as Write or All carry. The request was refused before it read or changed any scene, quota or credit balance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Scene not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Scene cannot be retrained in its current state, or its last failure would repeat (`retry_blocked`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"413":{"description":"`payload_too_large`: the scene's source file exceeds the 5.0 GB upload limit. No new version was created and nothing was charged.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"415":{"description":"The request has a body that isn't sent as `Content-Type: application/json`. `curl -d` sends form encoding unless you add that header. Nothing was created, claimed or charged.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"`source_unusable`: the scene's source file won't make a usable scene: it is empty, a video under 30 seconds, a 360° video, a photo upload that isn't a zip, or a zip with fewer than 20 JPEG or PNG photos (5 for a COLMAP model). `details` names the rule. No new version was created and nothing was charged. Create a new scene with a usable file.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"`queue_full`: as many of your scenes are processing as your plan allows at once. Nothing was charged, and `meta` holds `limit` and `current`. After `/process` the scene stays `uploading`: call `/process` again once one finishes. After `/retrain` no new version is kept, unless the response carries no `Retry-After`: then check `GET /v1/scenes` before sending it again. `quota_exceeded`: the monthly quota is used up; `meta` holds `period`, `limit` and `field`. After `/retrain` no new version is kept. `rate_limited`: more than 300 requests, or 30 create, `/process` and `/retrain` requests, this minute, counted per user. The request was refused before it read or changed any scene, quota or credit balance. Wait `Retry-After` seconds, then send it again. `meta.limit` and `X-RateLimit-Limit` name the limit that refused.","headers":{"Retry-After":{"description":"Seconds to wait before sending the request again: 120 with `queue_full`, 60 with `rate_limited`. Not sent with `quota_exceeded`.","schema":{"type":"integer","example":120}},"X-RateLimit-Limit":{"description":"Requests a minute the limit that refused allows. Every `/v1` response carries it, naming its route's limit.","schema":{"type":"integer","example":300}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"`internal_error`: an unexpected failure on our side. Contact support with `meta.request_id`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"502":{"description":"`upstream_error`: the GPU backend refused the launch, or the capture data could not be stored. The charge was refunded. Repeating the same `/process` request returns this saved failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"Temporarily unavailable, e.g. a database blip. Not an answer about your key or scene. No `Retry-After`: this operation doesn't dedupe repeats, and a repeated create or `/retrain` makes another scene. Check `GET /v1/scenes` before sending it again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/scenes/{id}/cancel":{"post":{"tags":["Scenes"],"summary":"Cancel scene processing","description":"Cancel an in-progress scene processing job. A cancel before the job starts, or within 10 seconds after, refunds the credits it charged. The job starts around when /process returns. A later cancel keeps the processing charge and refunds only an AI Enhance charge reserved at upload (dashboard launches; API launches reserve none).","operationId":"cancelScene","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","description":"Scene ID","example":"a1b2c3d4e5f6"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Processing cancelled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CancelSceneResponse"}}}},"400":{"description":"`invalid_input`: a path, query, header or body value is invalid. When it fails the schema, `error.details` lists one `{field, message}` per problem and `error.message` joins them.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Authentication required.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`insufficient_scope`: the API key lacks the `write:scenes` scope, which keys created as Write or All carry. The request was refused before it read or changed any scene, quota or credit balance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Scene not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Cannot cancel scene in current state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"`rate_limited`: more than 300 requests this minute, counted per user, or per client IP without a key. The request was refused before it read or changed any scene, quota or credit balance. Wait `Retry-After` seconds, then send it again. `meta.limit` and `X-RateLimit-Limit` name the limit that refused.","headers":{"Retry-After":{"description":"Seconds to wait before sending the request again.","schema":{"type":"integer","example":60}},"X-RateLimit-Limit":{"description":"Requests a minute the limit that refused allows. Every `/v1` response carries it, naming its route's limit.","schema":{"type":"integer","example":300}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"`internal_error`: an unexpected failure on our side. Contact support with `meta.request_id`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"Temporarily unavailable, e.g. a database blip. Not an answer about your key or scene. No `Retry-After`: this operation doesn't dedupe repeats, and a repeated create or `/retrain` makes another scene. Check `GET /v1/scenes` before sending it again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/scenes/{id}/download":{"get":{"tags":["Scenes"],"summary":"Download 3D model file","description":"Download the processed 3D Gaussian Splat model file.","operationId":"downloadScene","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","description":"Scene ID","example":"a1b2c3d4e5f6"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","enum":["sog","ply","spz"],"description":"File to download. Omit it for the default: `spz` if the scene has one, otherwise `sog`, then `ply`; the `X-Splat-Format` header names the file sent. `spz`: Niantic's compressed splat format, lossy, for viewers that load `.spz` files, such as native renderers. Older scenes, and scenes whose SPZ conversion failed, have none: `format=spz` answers 404 for them, never another format. `sog`: compressed, for web viewers. `ply`: raw splat at full precision. `sog` and `ply` fall back to each other.","example":"spz"},"required":false,"name":"format","in":"query"}],"responses":{"200":{"description":"Binary model file (application/octet-stream)."},"400":{"description":"`invalid_input`: a path, query, header or body value is invalid. When it fails the schema, `error.details` lists one `{field, message}` per problem and `error.message` joins them.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Authentication required.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`insufficient_scope`: the API key lacks the `read:scenes` scope, which keys created as Read, Write or All carry. The request was refused before it read or changed any scene, quota or credit balance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Scene not found, it has no model yet, or (format=spz) it has no SPZ.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"`rate_limited`: more than 300 requests this minute, counted per user, or per client IP without a key. The request was refused before it read or changed any scene, quota or credit balance. Wait `Retry-After` seconds, then send it again. `meta.limit` and `X-RateLimit-Limit` name the limit that refused.","headers":{"Retry-After":{"description":"Seconds to wait before sending the request again.","schema":{"type":"integer","example":60}},"X-RateLimit-Limit":{"description":"Requests a minute the limit that refused allows. Every `/v1` response carries it, naming its route's limit.","schema":{"type":"integer","example":300}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"`internal_error`: an unexpected failure on our side. Contact support with `meta.request_id`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"Temporarily unavailable, e.g. a database blip. Not an answer about your key or scene: retry the same request after `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying the same request.","schema":{"type":"integer","example":5}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/scenes/{id}/thumbnail":{"get":{"tags":["Scenes"],"summary":"Get scene thumbnail","description":"Get the PNG thumbnail for a scene. A public scene's needs no API key. A private scene's is served only to its owner's key: any other caller gets `404`, so whether a scene exists stays private. A deleted scene's is never served.","operationId":"getSceneThumbnail","security":[{},{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","description":"Scene ID","example":"a1b2c3d4e5f6"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"PNG thumbnail image. An old scene with no recorded thumbnail may answer its legacy JPEG; `Content-Type` names the type.","headers":{"Cache-Control":{"description":"`public, max-age=3600` for a public scene's thumbnail: a cache may keep it for an hour after the scene turns private or is deleted. `private, no-store` for a private scene's, which only its owner's key can fetch.","schema":{"type":"string"}}},"content":{"image/png":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"`invalid_input`: a path, query, header or body value is invalid. When it fails the schema, `error.details` lists one `{field, message}` per problem and `error.message` joins them.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No thumbnail: the scene doesn't exist, is deleted, or has no image, or it is private and the key is not its owner's. These are not told apart.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"`rate_limited`: more than 300 requests this minute, counted per user, or per client IP without a key. The request was refused before it read or changed any scene, quota or credit balance. Wait `Retry-After` seconds, then send it again. `meta.limit` and `X-RateLimit-Limit` name the limit that refused.","headers":{"Retry-After":{"description":"Seconds to wait before sending the request again.","schema":{"type":"integer","example":60}},"X-RateLimit-Limit":{"description":"Requests a minute the limit that refused allows. Every `/v1` response carries it, naming its route's limit.","schema":{"type":"integer","example":300}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"`internal_error`: an unexpected failure on our side. Contact support with `meta.request_id`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"Temporarily unavailable, e.g. a database blip. Not an answer about your key or scene: retry the same request after `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying the same request.","schema":{"type":"integer","example":5}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/usage":{"get":{"tags":["Usage"],"summary":"Get current usage and quotas","description":"Returns the authenticated user's usage for the current billing period and their plan limits.","operationId":"getUsage","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Current usage and limits.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageResponse"}}}},"401":{"description":"Authentication required.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"`rate_limited`: more than 300 requests this minute, counted per user, or per client IP without a key. The request was refused before it read or changed any scene, quota or credit balance. Wait `Retry-After` seconds, then send it again. `meta.limit` and `X-RateLimit-Limit` name the limit that refused.","headers":{"Retry-After":{"description":"Seconds to wait before sending the request again.","schema":{"type":"integer","example":60}},"X-RateLimit-Limit":{"description":"Requests a minute the limit that refused allows. Every `/v1` response carries it, naming its route's limit.","schema":{"type":"integer","example":300}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"`internal_error`: an unexpected failure on our side. Contact support with `meta.request_id`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"Temporarily unavailable, e.g. a database blip. Not an answer about your key or scene: retry the same request after `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying the same request.","schema":{"type":"integer","example":5}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}}},"webhooks":{"scene.completed":{"post":{"tags":["Webhooks"],"summary":"Scene completed","description":"A scene finished processing. Sent as a `POST` to the scene's `webhook_url`, with `Content-Type: application/json` and `User-Agent: Splat-Webhook/1.0`. Answer any 2xx to acknowledge it. Any other status, or a network error, is retried: 3 attempts in all, 1 s then 2 s apart, with the same body and signature. Redirects are followed and the last response counts: a 301, 302 or 303 turns the `POST` into a `GET` without the body, so register the final URL. Only scenes created with a `webhook_url` send events; a retrain's new version sends none, a cancel sends none, and a launch that fails at `/process`, which answers with an error, sends none. A scene reported failed whose job then finishes sends `scene.completed` after `scene.failed`, so read `GET /v1/scenes/{id}` for the current state.","operationId":"sceneCompleted","parameters":[{"schema":{"type":"string","description":"Sent only when the scene was created with a `webhook_secret`. Format `t=<unix seconds>,v1=<hex>`: `v1` is the HMAC-SHA256, keyed with your `webhook_secret`, of `<t>.<raw request body>`. To verify, compute it over the body bytes as received (not JSON you re-serialized), compare it with `v1` in constant time, and refuse a `t` older than 5 minutes, which stops replays. Retries carry the same `t`."},"required":false,"name":"X-Splat-Signature","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SceneCompletedEvent"}}}},"responses":{"200":{"description":"Any 2xx acknowledges the event. The body is ignored."}}}},"scene.failed":{"post":{"tags":["Webhooks"],"summary":"Scene failed","description":"A scene failed. Its credits are refunded. Sent as a `POST` to the scene's `webhook_url`, with `Content-Type: application/json` and `User-Agent: Splat-Webhook/1.0`. Answer any 2xx to acknowledge it. Any other status, or a network error, is retried: 3 attempts in all, 1 s then 2 s apart, with the same body and signature. Redirects are followed and the last response counts: a 301, 302 or 303 turns the `POST` into a `GET` without the body, so register the final URL. Only scenes created with a `webhook_url` send events; a retrain's new version sends none, a cancel sends none, and a launch that fails at `/process`, which answers with an error, sends none. A scene reported failed whose job then finishes sends `scene.completed` after `scene.failed`, so read `GET /v1/scenes/{id}` for the current state.","operationId":"sceneFailed","parameters":[{"schema":{"type":"string","description":"Sent only when the scene was created with a `webhook_secret`. Format `t=<unix seconds>,v1=<hex>`: `v1` is the HMAC-SHA256, keyed with your `webhook_secret`, of `<t>.<raw request body>`. To verify, compute it over the body bytes as received (not JSON you re-serialized), compare it with `v1` in constant time, and refuse a `t` older than 5 minutes, which stops replays. Retries carry the same `t`."},"required":false,"name":"X-Splat-Signature","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SceneFailedEvent"}}}},"responses":{"200":{"description":"Any 2xx acknowledges the event. The body is ignored."}}}}}}