{"openapi":"3.1.0","info":{"title":"WEARFITS Virtual Try-On API","version":"1.2.0","description":"AI-powered virtual try-on and digital twin generation service.\n\n## Features\n- **Virtual Fitting Pipeline**: Create a twin from face and body inputs, or reuse an existing twin, then generate a try-on image\n- **Digital Twin Caching**: 30-day cache for faster subsequent requests with same inputs\n- **Virtual Try-On**: Clothing (top, bottom, dress) and shoes try-on\n- **Digital Twin**: Generate reusable avatar images from face plus body photo, silhouette, measurements or clothing size\n- **Estimated Size Fitting**: Compare eligible twin body profiles with body-range size charts. Scores are estimates and can reveal underlying measurement information; they are not a privacy boundary\n- **Reference Images**: Support for packshot + on-model reference pairs for better results\n\n## Garment Images\nEach garment slot (`topGarment`, `bottomGarment`, `fullBodyGarment`, `shoes`) accepts either a single image URL/base64 string, or an array of 1-2 images:\n- **`[0]`** (required) — **packshot**: the primary product image used for try-on\n- **`[1]`** (optional) — **on-model reference**: an additional photo (e.g. garment worn by a model) to help AI understand fit and draping\n\nOrder matters, naming does not. Examples:\n```json\n{\n  \"personImages\": [\"https://example.com/person.jpg\"],\n  \"topGarment\": [\"https://example.com/shirt-packshot.jpg\", \"https://example.com/shirt-on-model.jpg\"]\n}\n```\n\n## Retrying Failed Jobs\nIf a job fails (`job.failed` webhook), inspect its error before retrying. Correct permanent input or account problems first. Resubmission may incur usage; endpoint-specific deduplication and caches can reuse existing jobs or results. Failed jobs have reached a terminal outcome after applicable retries, or stopped early on a non-retryable failure.\n\n## Authentication\nThis OpenAPI document applies `apiKey` as its default security requirement.\nProtected operations require an API key in `X-API-Key` unless their operation-level\n`security` overrides that default. Scoped storefront tokens (wfs1.) can use\n`X-API-Key` on their allowed routes. Account tokens (wfa1.) belong in\n`X-Wearfits-Account` alongside a service API key; they are rejected in `X-API-Key`.\nThese authentication modes have different route permissions. Renewed tokens retain\njob access only within the same account and authentication mode; API-key jobs require\nthe original individual key. Public\noperations and routes that use a separate credential declare those exceptions\nat operation level; check each operation's effective security instead of\nassuming one authentication scheme for every route. An empty operation\n`security` array means no registered OpenAPI header scheme applies; the\noperation may still require signed query parameters or IP allowlisting stated\nin its operation or tag description. A security alternatives array containing\nboth `{ apiKey: [] }` and an empty object preserves the API-key Authorize path\nwhile allowing separately documented credentials such as signed query\nparameters.\n\nCredential kinds:\n1. **User keys** — created at dash.wearfits.com. Count both API-key usage and the monthly AI quota.\n2. **Storefront tokens** (`wfs1.`) — short-lived, minted at `POST /api/v1/storefront/tokens`. Browser widget only: `POST /api/v1/virtual-fitting` and `GET /api/v1/jobs/{id}`. Count the AI quota, not the key-usage counter.\n3. **Service keys** — server-to-server. Uncounted unless paired with `X-Wearfits-Account: wfa1.` (then the AI quota is counted).\n4. **Account tokens** (`wfa1.`) — never sent as `X-API-Key`; only as `X-Wearfits-Account` next to a service key.\n5. **Usage tokens** (`wfu1.`) — analytics-only attribution, minted only with the dedicated `USAGE_TOKEN_HANDSHAKE` secret, sent as `X-Wearfits-Usage-Account` next to a service key. No quota, no change to job or model ownership; ignored when invalid, refused anywhere else.\n\nDo not embed an account-issued dashboard key in a shopper page. Mint a storefront token server-side, or proxy with a service key plus `X-Wearfits-Account`.\n\n### Job ownership (same-key polling)\n\nJobs are owned by the **credential that submitted them**, not by a shared service key. `GET /api/v1/jobs/{jobId}`, `GET /api/v1/jobs/{jobId}/trace` and `DELETE /api/v1/jobs/{jobId}` compare the caller's key hash against the submitting hash; any other credential — including another valid key on the same account — receives the **same 404** as a non-existent job (never 403, so the response cannot be used to probe job ids).\n\nStorefront and account-attribution tokens hash a stable per-account namespace (`storefront:<userId>` / `account:<userId>`), so reminting still polls in-flight jobs for that merchant, and two merchants sharing a service key cannot see each other's jobs.\n\n**Key-rotation caveat:** rotating a database-backed merchant key orphans jobs that are still in flight — the new key gets 404 for them. Finish polling (and cancelling) in-flight jobs with the key that submitted them, then switch. Long-running jobs such as shoe-3d, which can take several minutes, are the practical case to watch.\n\n## File Access\nInternal files (cached digital twins) are not accessible via the API.\nResult files accept either the same submitting `X-API-Key` or a valid signed\n`token` + `expires` pair when the operation documents that alternative.\n\n## Rate Limits\n- Job submission: 50 requests/minute\n- Job status: 500 requests/minute\n- File downloads: 300 requests/minute\n\nRate limit headers: `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`\n\n## Webhooks\nYou can provide a webhook URL when submitting requests to receive notifications when jobs complete.\nWebhooks are signed with HMAC-SHA256 via `X-Webhook-Signature` header using a **per-client signing secret**.\n\n**Ordinary private API-key submissions:** use the UTF-8 bytes of the lowercase hexadecimal SHA-256 digest of the submitting API key as the HMAC key. Do not decode that hex string into binary bytes. The signature header is `sha256=` followed by lowercase hexadecimal HMAC output.\n\n**Account/storefront attribution:** the signing value is instead SHA-256 of `account:<userId>` or `storefront:<userId>`. These identifier-derived values are not private secrets and must not be treated as proof of webhook origin. Prefer authenticated polling for these modes. Legacy records without an ownership hash emit an unsigned checksum; do not accept it as authentication.\n\nThe signed bytes differ by callback type:\n- **Job callbacks** (job.completed/job.failed): parse JSON, recursively sort object keys while preserving array order, then serialize compactly with JavaScript JSON.stringify semantics and UTF-8 encode. JavaScript object enumeration, Unicode and number serialization must be preserved; generic sorted JSON in another language is not necessarily equivalent. Do not sign the received raw job body.\n- **Batch callbacks** (batch.completed): sign the exact received body bytes, without sorting or reserialization.\n\nCompare signatures in constant time, validate X-Webhook-Timestamp against the signed payload timestamp and your replay window, and deduplicate by job/batch ID and event. Job deliveries make up to three attempts; batch delivery has no retry recovery. Keep polling as a fallback. See https://api.wearfits.com/llms-full.txt for the same authentication and callback contract.","contact":{"name":"WEARFITS Support","url":"https://wearfits.com/contact?s=support"}},"servers":[{"url":"https://api.wearfits.com","description":"Production"}],"tags":[{"name":"Health","description":"Health check endpoints"},{"name":"Virtual Fitting","description":"Complete pipeline: face + silhouette/photo/measurements → digital twin → try-on (with 30-day caching)"},{"name":"Try-On","description":"Virtual try-on for clothing and shoes"},{"name":"Digital Twin","description":"Generate digital twin from face + depth silhouette"},{"name":"Size Fitting","description":"Synchronous, non-billable estimated size recommendations for eligible measurement-based twins"},{"name":"Generate Model","description":"Generate full-body model from face photo"},{"name":"Jobs","description":"Job management endpoints. Virtual-fitting and digital-twin progress is indeterminate until completion (100%); render a spinner when percentage is absent."},{"name":"Storefront","description":"Mint short-lived storefront (`wfs1.`) and account-attribution (`wfa1.`) tokens so a shopper browser never holds an exhaustible dashboard API key."},{"name":"Product Discovery","description":"Crawl a shop URL with Cloudflare Browser Rendering and return 3–4 packshot images of the first matching product (shoes or bags) — ready to feed into the 3D generation endpoint."},{"name":"Digitization","description":"Bulk shoe-to-3D pipeline. Submit many products in one request and poll a batch endpoint for progress. Each item runs through the same shoe-3d processor as single requests (validation, AI correction, WEARFITS upload) but on a lower-priority queue so it never competes with real-time try-on traffic. Up to 500 products per batch; results expire after 7 days."},{"name":"Texture Painter","description":"AI-powered texture enhancement for the browser-based texture painting tool (no auth required)"},{"name":"Files","description":"File delivery. Individual operations declare same-key API access, signed query parameters, or both."},{"name":"Monitoring","description":"Operator dashboard operations restricted by the deployment IP allowlist. They do not use X-API-Key."}],"security":[{"apiKey":[]}],"components":{"securitySchemes":{"apiKey":{"type":"apiKey","in":"header","name":"X-API-Key","description":"WEARFITS API key in X-API-Key. Scoped storefront tokens (wfs1.) may use this header on allowed routes. Account tokens (wfa1.) must instead use X-Wearfits-Account alongside a service API key; they are rejected in X-API-Key."},"AdminKey":{"type":"apiKey","in":"header","name":"X-Admin-Key","description":"Admin-only key for foot-measurement admin endpoints (`GET /recent`, `DELETE /{id}`). Separate from the SPA-facing `X-API-Key` — set via `wrangler secret put FOOT_MEASUREMENT_ADMIN_KEY`. Routes return 503 when this secret is unset on the deployment."}},"schemas":{"HealthResponse":{"type":"object","properties":{"status":{"type":"string","enum":["ok"]},"timestamp":{"type":"string","format":"date-time"},"version":{"type":"string"}},"required":["status","timestamp","version"]},"WarmupResponse":{"type":"object","properties":{"status":{"type":"string","enum":["ok"]},"timestamp":{"type":"string","format":"date-time"},"modal":{"type":"object","properties":{"warm":{"type":"boolean"},"responseTimeMs":{"type":"number"},"error":{"type":"string"}},"required":["warm"]},"queue":{"type":"object","properties":{"warm":{"type":"boolean"}},"required":["warm"]}},"required":["status","timestamp","modal","queue"]},"QueueWarmupResponse":{"type":"object","properties":{"status":{"type":"string","enum":["ok"]},"timestamp":{"type":"string","format":"date-time"},"queueWarm":{"type":"boolean","enum":[true]}},"required":["status","timestamp","queueWarm"]},"ErrorResponse":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"object","properties":{"code":{"type":"string","example":"VALIDATION_ERROR"},"message":{"type":"string","example":"Invalid request body"},"details":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"message":{"type":"string"}},"required":["field","message"]}}},"required":["code","message"]}},"required":["success","error"]},"PackshotCleanup":{"type":"object","properties":{"status":{"type":"string","enum":["applied","noop","unreported","unavailable","failed"],"description":"`applied` = ghost pixels were repainted; `noop` = cleanup ran and reported nothing to remove; `unreported` = the backend returned an image but NO cleanup result, i.e. it likely ignored `clean_packshot` entirely — NOT a healthy outcome, the packshot went downstream unverified; `unavailable` = image-processing backend not configured; `failed` = cleanup errored and the RAW packshot went to fal.ai unchecked."},"numComponents":{"type":"number","description":"Connected components found in the packshot (when cleanup ran)."},"areaRatio":{"type":"number","description":"Area ratio of the kept component (when cleanup ran)."},"errorCode":{"type":"string","description":"Error CODE (e.g. `HTTP_500`, `SERVER_ERROR`) when `status` is `failed`. Never provider prose."}},"required":["status"],"description":"Outcome of the deterministic packshot cleanup (ghost-duplicate removal) that runs on the AI packshot before it is sent to fal.ai. Absent on legacy entries (written before this field existed) — absence means \"unknown\", not \"clean\". `failed` means the cleanup call errored and the RAW packshot went to fal.ai unchecked, so any hallucinated ghost shoes were digitized into the 3D model; `errorCode` carries the failure CODE. `unreported` means the backend answered without a cleanup result — the step most likely never ran (a deployment that ignores `clean_packshot`), so it is a degradation, not a clean run."},"PackshotSingleShoe":{"type":"object","properties":{"status":{"type":"string","enum":["single","regenerated","multiple_after_retries","unavailable"],"description":"`single` = the first packshot showed one shoe; `regenerated` = an earlier packshot showed two or more shoes and a regenerated one showed one; `multiple_after_retries` = every generation showed two or more shoes and the last one was used anyway; `unavailable` = the count check failed and the pipeline proceeded without it (fail-open)."},"count":{"type":"integer","description":"Shoe count reported for the packshot that was used (when the check answered)."},"attempts":{"type":"integer","description":"Packshot generations performed (1-3)."},"errorCode":{"type":"string","description":"Error CODE: the count failure when `status` is `unavailable`, or the regeneration failure when `stopReason` is `regeneration_failed`. Never provider prose."},"stopReason":{"type":"string","enum":["regeneration_failed","deadline","sync_path"],"description":"Why a `multiple_after_retries` outcome stopped before the full budget: a regeneration failed (the previous multi-shoe packshot is used), the guard time budget ran out, or the synchronous pre-validation path (which never regenerates)."},"regeneratedFromCount":{"type":"integer","description":"First count of two or more shoes when a later packshot was used (`regenerated` / `unavailable`)."}},"required":["status","attempts"],"description":"Outcome of the single-shoe guard (vision count of the cleaned packshot, regenerating up to twice on two or more shoes) before fal.ai. Absent on legacy entries and non-shoe products — absence means \"unknown\". `multiple_after_retries` means the packshot sent to fal.ai still showed several shoes."},"JobResult":{"type":"object","properties":{"url":{"type":"string","format":"uri","example":"https://results.wearfits.com/result-123.png"},"expiresAt":{"type":"string","format":"date-time","example":"2024-01-02T12:00:00Z"},"digitalTwinId":{"type":"string","pattern":"^[a-f0-9]{64}$","description":"ID of the generated or reused digital twin (SHA-256 hex), reusable while its cache entry remains available"}},"required":["url","expiresAt"]},"TryOnResponse":{"type":"object","properties":{"success":{"type":"boolean","enum":[true]},"jobId":{"type":"string","format":"uuid","example":"550e8400-e29b-41d4-a716-446655440000"},"status":{"type":"string","enum":["queued","completed"]},"estimatedProcessingTime":{"type":"number","description":"Estimated processing time in seconds","example":30},"statusUrl":{"type":"string","format":"uri","description":"URL to check job status","example":"https://api.wearfits.com/api/v1/jobs/550e8400-e29b-41d4-a716-446655440000"},"results":{"type":"array","items":{"$ref":"#/components/schemas/JobResult"},"description":"Results (returned immediately if cached)"}},"required":["success","jobId","status","estimatedProcessingTime","statusUrl"]},"ClothingTryOnRequest":{"type":"object","properties":{"personImages":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":3,"description":"Person photos (URLs or base64) for face and/or silhouette. Required if digitalTwinId is not provided.","example":["https://example.com/person.jpg"]},"digitalTwinId":{"type":"string","pattern":"^[a-f0-9]{64}$","description":"Unique identifier for a previously generated digital twin (SHA-256 hex hash)","example":"24e73099501085bae09d26bcdc53624142cac544d5b015c599e498df8a9f6b5e"},"topGarment":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":2,"description":"Array of 1-2 images (URLs or base64): first is packshot, second (optional) is on-model reference","example":["https://example.com/packshot.jpg","https://example.com/on-model.jpg"]}],"description":"Top garment (shirt, blouse, etc.) - single URL or array of [packshot, on-model-reference]","example":["https://example.com/top-packshot.jpg","https://example.com/top-on-model.jpg"]},"bottomGarment":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":2,"description":"Array of 1-2 images (URLs or base64): first is packshot, second (optional) is on-model reference","example":["https://example.com/packshot.jpg","https://example.com/on-model.jpg"]}],"description":"Bottom garment (pants, skirt, etc.) - single URL or array of [packshot, on-model-reference]","example":"https://example.com/bottom.jpg"},"fullBodyGarment":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":2,"description":"Array of 1-2 images (URLs or base64): first is packshot, second (optional) is on-model reference","example":["https://example.com/packshot.jpg","https://example.com/on-model.jpg"]}],"description":"Full-body garment (dress, jumpsuit, etc.) - single URL or array of [packshot, on-model-reference]","example":["https://example.com/dress-packshot.jpg","https://example.com/dress-on-model.jpg"]},"shoes":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":2,"description":"Array of 1-2 images (URLs or base64): first is packshot, second (optional) is on-model reference","example":["https://example.com/packshot.jpg","https://example.com/on-model.jpg"]}],"description":"Optional shoes to add to any outfit - single URL or array of [packshot, on-model-reference]","example":"https://example.com/heels.jpg"},"productImage":{"type":"string","description":"Single product image (explicit opt-in for direct 2-image path without garment grid). Pair with `personImages` (no digital twin) OR `digitalTwinId` (cached twin image is used as the person side). Cannot be combined with garment slot fields (`topGarment`, `bottomGarment`, `fullBodyGarment`, `shoes`).","example":"https://example.com/product.jpg"},"productCategory":{"type":"string","enum":["auto","top","bottom","full-body","shoe"],"default":"auto","description":"Category hint for productImage (`auto` lets the model infer placement).","example":"auto"},"options":{"type":"object","properties":{"quality":{"type":"string","enum":["draft","standard","high"],"default":"standard","description":"Output quality level"},"preserveBackground":{"type":"boolean","default":true,"description":"Whether to preserve the original background"},"skipResultCache":{"type":"boolean","default":false,"description":"If true, skip final result cache and always generate fresh try-on"},"provider":{"type":"string","enum":["openrouter","openai","xai"],"description":"Specific provider to use (optional)"},"model":{"type":"string","description":"Pin a specific OpenRouter model id (e.g. `x-ai/grok-imagine-image-2.0`, `google/gemini-3.1-flash-image`). When set, the default model chain is bypassed and only this model is requested. Intended for testing / A/B comparisons — production traffic should leave this unset to use the cost-optimized fallback chain. The pinned model must accept at least as many `input_references` as this call site sends (person + grid, i.e. 2-3): `microsoft/mai-image-2.5` caps at 1 and is rejected here with `INPUT_REFERENCE_LIMIT` before any HTTP call — see MODEL_INPUT_REFERENCE_LIMITS in src/services/openrouter-images.ts.","example":"x-ai/grok-imagine-image-2.0"},"promptPreset":{"type":"string","enum":["clothing-1"],"default":"clothing-1","description":"Prompt style preset for clothing try-on."}}},"webhookUrl":{"type":"string","format":"uri","description":"Webhook URL (HTTPS only) to notify when job is complete","example":"https://your-app.com/webhooks/wearfits"}}},"ShoeTryOnRequest":{"type":"object","properties":{"personImages":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":2,"description":"Person/feet photos (URLs or base64). Required if digitalTwinId is not provided.","example":["https://example.com/feet.jpg"]},"digitalTwinId":{"type":"string","pattern":"^[a-f0-9]{64}$","description":"Unique identifier for a previously generated digital twin (SHA-256 hex hash)","example":"24e73099501085bae09d26bcdc53624142cac544d5b015c599e498df8a9f6b5e"},"shoeImages":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string"},"angle":{"type":"string","enum":["front","side","back"],"description":"Angle of the shoe image"}},"required":["url"]},"minItems":1,"maxItems":4,"description":"Shoe images (URLs or base64) from various angles","example":[{"url":"https://example.com/shoe-front.jpg","angle":"front"}]},"options":{"type":"object","properties":{"quality":{"type":"string","enum":["draft","standard","high"],"default":"standard"},"provider":{"type":"string","enum":["openrouter","openai","xai"]},"model":{"type":"string","description":"Pin a specific OpenRouter model id. When set, the default model chain is bypassed. Intended for testing."}}},"webhookUrl":{"type":"string","format":"uri","description":"Webhook URL (HTTPS only) to notify when job is complete","example":"https://your-app.com/webhooks/wearfits"}},"required":["shoeImages"]},"GenerateResponse":{"type":"object","properties":{"success":{"type":"boolean","enum":[true]},"jobId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["queued"]},"estimatedProcessingTime":{"type":"number","description":"Estimated time in seconds"},"statusUrl":{"type":"string","format":"uri"}},"required":["success","jobId","status","estimatedProcessingTime","statusUrl"]},"GenerateRequest":{"type":"object","properties":{"prompt":{"type":"string","minLength":1,"maxLength":2000,"description":"Text prompt describing what to generate or how to modify the image","example":"Generate a red Adidas running shoe on white background"},"inputImages":{"type":"array","items":{"type":"string","format":"uri"},"maxItems":4,"description":"Optional input images to use as reference or to modify","example":["https://example.com/shoe.jpg"]},"options":{"type":"object","properties":{"model":{"type":"string","description":"Model to use for image generation"},"quality":{"type":"string","enum":["draft","standard","high"],"default":"standard"}}},"webhookUrl":{"type":"string","format":"uri","description":"Webhook URL (HTTPS only) to notify when job is complete","example":"https://your-app.com/webhooks/wearfits"}},"required":["prompt"]},"DigitalTwinResponse":{"type":"object","properties":{"success":{"type":"boolean","enum":[true]},"jobId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["queued","completed"]},"estimatedProcessingTime":{"type":"number","description":"Estimated time in seconds"},"statusUrl":{"type":"string","format":"uri"},"mode":{"type":"string","enum":["direct","photo","measurements","clothing_size"],"description":"Processing mode: direct (pre-rendered silhouette), photo (automatic SAM-3D extraction), measurements (body from measurements), or clothing_size (body from clothing size)"},"digitalTwinId":{"type":"string","description":"Unique ID for the generated digital twin (returned immediately if cached)"}},"required":["success","jobId","status","estimatedProcessingTime","statusUrl","mode"]},"BodyMeasurements":{"type":"object","properties":{"height":{"type":"number","minimum":140,"maximum":210,"description":"Body height in cm (140-210)","example":170},"chest":{"type":"number","minimum":70,"maximum":160,"description":"Chest/bust circumference in cm (70-160)","example":90},"waist":{"type":"number","minimum":55,"maximum":170,"description":"Waist circumference in cm (55-170)","example":70},"hip":{"type":"number","minimum":70,"maximum":170,"description":"Hip circumference in cm (70-170)","example":95},"inseam":{"type":"number","minimum":60,"maximum":100,"description":"Inseam length in cm (60-100)","example":75}},"description":"**[Measurements Mode]** Generate body silhouette from measurements instead of a photo.\n\n**What to provide:** At least 2 body measurements in centimeters:\n- height: Body height (140-210 cm)\n- chest: Chest/bust circumference (70-160 cm)\n- waist: Waist circumference (55-170 cm)\n- hip: Hip circumference (70-170 cm)\n- inseam: Inseam length (60-100 cm)\n\nThese are validation bounds, not the range the body dataset can represent. Matching is\nnearest-neighbour against a reference body dataset that never rejects a query, so a value\ninside the bounds but outside the data is served by extrapolation to the closest available\nbody. The dataset's real spread is per gender, and much narrower than the bounds:\n\n- female (n=800): height 140.9-184.5, chest 77.1-153.9, waist 64.1-147.0, hip 80.7-158.5, inseam 62.7-89.5 cm\n- male (n=1218): height 154.6-197.5, chest 82.3-152.4, waist 66.4-164.3, hip 83.6-166.9, inseam 66.7-94.5 cm\n\nThe gender field is optional. Omitting it widens the searched population to the union of both\ngenders, so a value that would be extrapolation for one gender can come back in range\nbecause a subject of the other gender matched it.\n\n**What happens:** The API:\n1. Finds the closest matching body from our dataset\n2. Generates a 3D mesh with the matched body shape\n3. Applies the specified pose\n4. Creates the digital twin\n\n**Use this if:** You have body measurement data (e.g., from a sizing profile) but no full-body photo.\n\n**Note:** Results are approximations based on nearest-neighbor matching."},"ClothingSize":{"type":"object","properties":{"height":{"type":"number","minimum":140,"maximum":210,"description":"Body height in cm (140-210)","example":170},"size":{"type":"string","enum":["XS","S","M","L","XL","XXL","3XL"],"description":"Typical clothing size worn (XS, S, M, L, XL, XXL, 3XL)","example":"M"}},"required":["height","size"],"description":"**[Clothing Size Mode]** Generate body from clothing size instead of exact measurements.\n\n**What to provide:**\n- height: Body height in cm (140-210)\n- size: Clothing size (XS, S, M, L, XL, XXL, 3XL)\n\n**What happens:** The API:\n1. Converts size to body measurements using market-average data from major brands\n2. Finds the closest matching body from our dataset\n3. Generates a 3D mesh with the matched body shape\n4. Creates the digital twin\n\n**Use this if:** You only know the user's clothing size, not their exact measurements.\n\n**Note:** Requires `gender` field to be set (defaults to female if not provided).\nMarket averages from: H&M, Uniqlo, ASOS, Nike, Adidas, Zara, Gap, Shein, Boohoo, Lululemon."},"DigitalTwinRequest":{"type":"object","properties":{"faceImage":{"type":"string","description":"Face photo for identity preservation (URL or base64). Use a well-lit, front-facing photo. Required in every mode, together with exactly one of bodyPhotoUrl, silhouetteImage, bodyMeasurements or clothingSize.","example":"https://example.com/face-selfie.jpg"},"bodyPhotoUrl":{"type":"string","description":"**[Photo Mode - RECOMMENDED]** Full-body photo (URL or base64) taken with a camera/phone.\n\n**What to provide:** A normal photograph showing the entire person standing, from head to toe.\n\n**What happens:** The API automatically:\n1. Extracts a 3D body mesh using SAM-3D\n2. Applies a standard pose for try-on\n3. Generates the digital twin\n\n**Requirements:**\n- Must show FULL BODY (head to feet visible)\n- Person should be standing\n- Good lighting, minimal obstructions\n\n**Use this if:** You're building a consumer app where users upload photos from their phone.","example":"https://example.com/user-full-body-photo.jpg"},"poseId":{"type":"string","enum":["standing_arms_down","man_pose","girl_pose","shoe_girl_pose","walking_pose","default"],"default":"default","description":"[Photo, Measurements and Clothing Size Modes] Target pose for a new digital twin. Ignored for a pre-rendered silhouette. Each resolved pose creates a separately cached twin.\n\n**Available poses:**\n- `default`: Resolved from deployment configuration and gender\n- `girl_pose`: Default female standing pose\n- `standing_arms_down`: Natural standing with arms at sides\n- `man_pose`: Default male standing pose\n- `shoe_girl_pose`: Pose optimized for shoe try-on (visible feet)\n\n**Caching:** Same face + body + pose = same cached digitalTwinId. Different poses create different twins.","example":"default"},"silhouetteImage":{"type":"string","description":"**[Direct Mode - Advanced]** Pre-rendered depth/silhouette image (URL or base64) from your own 3D pipeline.\n\n**What to provide:** A depth-rendered visualization of a 3D body mesh (NOT a regular photo).\n\n**What happens:** The API skips body extraction and uses your silhouette directly for twin generation.\n\n**Use this if:** You have your own 3D body scanning/rendering pipeline and want to provide pre-processed silhouettes.\n\n**Note:** Most applications should use Photo Mode (bodyPhotoUrl) instead.","example":"https://example.com/depth-render.png"},"bodyMeasurements":{"$ref":"#/components/schemas/BodyMeasurements"},"clothingSize":{"$ref":"#/components/schemas/ClothingSize"},"gender":{"type":"string","enum":["male","female","neutral"],"description":"Gender for prompt building. If not provided, uses neutral pronoun.","example":"female"},"prompt":{"type":"string","minLength":1,"maxLength":2000,"description":"Optional additional styling prompt. Will be combined with default digital twin prompt. Use this to customize clothing, background, etc.","example":"wearing casual jeans and white t-shirt, outdoor setting"},"options":{"type":"object","properties":{"quality":{"type":"string","enum":["draft","standard","high"],"default":"standard","description":"Quality tier affects processing time"},"seed":{"type":"integer","description":"Random seed for reproducible generation (not guaranteed with all models)"},"skipCache":{"type":"boolean","default":false,"description":"If true, skip cache and always generate fresh digital twin"},"promptPreset":{"type":"string","enum":["twin-1"],"default":"twin-1","description":"Prompt style preset for digital twin generation."},"preservePose":{"type":"boolean","default":false,"description":"[Photo Mode only] If true, preserve the original pose from bodyPhotoUrl instead of applying a standard pose. Useful when you want the digital twin to match the exact pose in the input photo."},"provider":{"type":"string","enum":["openrouter","openai"],"description":"Image provider override for digital twin generation."}},"description":"Generation options"},"webhookUrl":{"type":"string","format":"uri","description":"Webhook URL (HTTPS only) to notify when job is complete","example":"https://your-app.com/webhooks/wearfits"}}},"DigitalTwinStatusResponse":{"type":"object","properties":{"success":{"type":"boolean","enum":[true]},"digitalTwinId":{"type":"string","description":"The digital twin ID that was checked"},"valid":{"type":"boolean","description":"Whether the digital twin exists and is available for use"},"createdAt":{"type":"string","description":"When the digital twin was created (ISO 8601)"},"expiresAt":{"type":"string","description":"When the digital twin will expire (ISO 8601)"}},"required":["success","digitalTwinId","valid"]},"Generate3DResponse":{"type":"object","properties":{"success":{"type":"boolean","enum":[true]},"jobId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["queued"]},"estimatedProcessingTime":{"type":"number","description":"Estimated time in seconds"},"statusUrl":{"type":"string","format":"uri"}},"required":["success","jobId","status","estimatedProcessingTime","statusUrl"]},"Generate3DRequest":{"type":"object","properties":{"model":{"type":"string","enum":["fal-ai/hunyuan-3d/v3.1/pro/image-to-3d","fal-ai/bytedance/seed3d/image-to-3d","fal-ai/trellis","fal-ai/stable-fast-3d"],"default":"fal-ai/hunyuan-3d/v3.1/pro/image-to-3d","description":"The 3D generation model to use. Optional — a default model is used if omitted."},"inputImageUrl":{"type":"string","format":"uri","description":"URL of the image to convert to 3D","example":"https://example.com/shoe.png"},"options":{"type":"object","properties":{"faceCount":{"type":"number","minimum":40000,"maximum":1500000,"default":50000,"description":"Target polygon count"},"enablePbr":{"type":"boolean","default":true,"description":"Enable PBR material generation"},"generateType":{"type":"string","enum":["Normal","LowPoly","Geometry"],"default":"Normal","description":"Generation type: Normal=textured, LowPoly=reduced, Geometry=white"},"backImageUrl":{"type":"string","format":"uri","description":"Optional rear view image"},"leftImageUrl":{"type":"string","format":"uri","description":"Optional left view image"},"rightImageUrl":{"type":"string","format":"uri","description":"Optional right view image"},"uploadToWearFits":{"type":"boolean","default":false,"description":"Upload the generated 3D model to WEARFITS for shoe try-on"},"rightShoe":{"type":"boolean","default":false,"description":"Specifies if this is a right shoe model (for WEARFITS upload)"}}},"webhookUrl":{"type":"string","format":"uri","description":"Webhook URL (HTTPS only) to notify when job is complete","example":"https://your-app.com/webhooks/wearfits"}},"required":["inputImageUrl"]},"Shoe3DResponse":{"type":"object","properties":{"success":{"type":"boolean","enum":[true]},"jobId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["queued"]},"models":{"type":"array","items":{"type":"string"},"description":"Models that will be used for generation"},"estimatedProcessingTime":{"type":"number","description":"Estimated time in seconds (based on slowest model)"},"statusUrl":{"type":"string","format":"uri"}},"required":["success","jobId","status","models","estimatedProcessingTime","statusUrl"]},"ShoeValidationIssue":{"type":"object","properties":{"code":{"type":"string","enum":["BAD_LIGHTING","BUSY_BACKGROUND","MULTIPLE_SHOES","SHOE_NOT_FULL_FRAME","SHOE_CROPPED","REFLECTIVE_LIGHTS","PEOPLE_OR_HANDS","TEXT_OR_WATERMARKS","BLURRY_IMAGE","NOT_A_SHOE","BACK_VIEW_ONLY","SOLE_VIEW_ONLY"],"description":"Issue code identifying the problem"},"message":{"type":"string","description":"Human-readable description of the issue"},"severity":{"type":"string","enum":["error","warning"],"description":"Severity level - errors prevent processing, warnings are informational"}},"required":["code","message","severity"]},"Shoe3DValidationError":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"object","properties":{"code":{"type":"string","enum":["SHOE_IMAGE_VALIDATION_FAILED"]},"message":{"type":"string","description":"Human-readable error message","example":"No suitable shoe image found."},"details":{"type":"array","items":{"$ref":"#/components/schemas/ShoeValidationIssue"},"description":"Detailed list of validation issues"}},"required":["code","message","details"]},"validation":{"type":"object","properties":{"isValid":{"type":"boolean","enum":[false]},"bestImageIndex":{"type":"integer","minimum":0},"isRightShoe":{"type":"boolean","nullable":true},"confidence":{"type":"number","minimum":0,"maximum":1},"issues":{"type":"array","items":{"$ref":"#/components/schemas/ShoeValidationIssue"}},"processingTimeMs":{"type":"number"}},"required":["isValid","bestImageIndex","isRightShoe","confidence","issues","processingTimeMs"],"description":"Full validation result for debugging"}},"required":["success","error","validation"]},"ShoePreValidation":{"type":"object","properties":{"bestImageIndex":{"type":"integer","minimum":0,"description":"0-based index of the best image for 3D generation"},"isRightShoe":{"type":"boolean","nullable":true,"description":"Whether the shoe is a right shoe (true), left shoe (false), or unknown (null)"},"confidence":{"type":"number","minimum":0,"maximum":1,"description":"Confidence score of the validation (0.0-1.0)"},"issues":{"type":"array","items":{"$ref":"#/components/schemas/ShoeValidationIssue"},"description":"List of warnings found during validation (errors would have rejected the request)"},"processingTimeMs":{"type":"number","description":"Time spent on validation in milliseconds"},"correctionApplied":{"type":"boolean","description":"Whether AI image correction was applied"},"correctedImageUrl":{"type":"string","description":"URL or base64 of the AI-corrected packshot image (if correction was applied)"},"correctionModelUsed":{"type":"string","description":"Actual image-generation model used to create the corrected packshot"},"correctionProcessingTimeMs":{"type":"number","description":"Time spent on image correction in milliseconds"},"packshotCleanup":{"allOf":[{"$ref":"#/components/schemas/PackshotCleanup"},{"description":"Outcome of the deterministic packshot cleanup that ran inside correction."}]},"packshotSingleShoe":{"allOf":[{"$ref":"#/components/schemas/PackshotSingleShoe"},{"description":"Outcome of the single-shoe guard that ran inside correction (absent = unknown)."}]}},"required":["bestImageIndex","isRightShoe"],"description":"Pre-validation result from synchronous image validation. Auto-populated by the API when validation passes. Do not set this manually."},"Shoe3DRequest":{"type":"object","properties":{"images":{"type":"array","items":{"type":"string"},"maxItems":10,"description":"Product images as URLs or base64 data URLs (up to 10). Image validation selects the best input for generation; AI correction can combine reference images. Supply images for photo-based generation, or omit them when processing glbInput. For bags and other products, set options.productType to other or auto.","example":["https://example.com/shoe-front.jpg","https://example.com/shoe-side.jpg"]},"models":{"type":"array","items":{"type":"string","enum":["fal-ai/hunyuan-3d/v3.1/pro/image-to-3d","fal-ai/tripo3d/h3.1/image-to-3d"]},"minItems":1,"maxItems":1,"default":["fal-ai/hunyuan-3d/v3.1/pro/image-to-3d"],"description":"Primary 3D generation model. Kept as a single-element array for backwards-compatibility. Supported values: `fal-ai/hunyuan-3d/v3.1/pro/image-to-3d` (default) and `fal-ai/tripo3d/h3.1/image-to-3d` (alternate). On model-scoped generation failures the pipeline automatically falls back to the other model; up to 4 attempts are made in total, alternating primary → fallback → primary → fallback. Account-level provider failures stop the chain immediately. Optional — the default is used if omitted."},"glbInput":{"type":"string","description":"Optional URL or base64 data URL to an existing GLB model. Skips AI geometry generation. Pre-simplification targets 100k triangles on a best-effort basis; if no usable simplified model URL is returned, processing continues with the original, so this is not an enforced maximum. Further processing follows options.enhanceTexture, options.smoothNormals and options.uploadToWearFits. Prefer uploading through POST /api/v1/files/upload and passing the returned URL to avoid large JSON payloads.","example":"https://example.com/shoe.glb"},"options":{"type":"object","properties":{"uploadToWearFits":{"type":"boolean","default":false,"description":"Upload the generated 3D model to WEARFITS for shoe try-on"},"validateInQueue":{"type":"boolean","default":true,"description":"If true (default), run image validation and AI correction asynchronously in the queue. The request returns immediately with a job ID, and validation/correction happens during processing. If false, validation runs synchronously and returns 400 if no valid image is found."},"skipCache":{"type":"boolean","default":false,"description":"If true, skip cache and always generate fresh 3D model. Cache key is based on input image URL + model name."},"correctImage":{"type":"boolean","default":true,"description":"If true (default), use AI (Gemini 3.1 Flash) to generate an ideal packshot from the uploaded photo(s) before validation and 3D generation. The generated image will have a clean background, proper framing, and studio-quality lighting. This can improve 3D generation results for user photos that have suboptimal backgrounds or lighting. Set to false to skip correction."},"preValidation":{"$ref":"#/components/schemas/ShoePreValidation"},"enhanceTexture":{"type":"boolean","default":true,"description":"Enhance GLB texture using AI with input images as references. Renders views (4 default, 6 in high quality), sends to Gemini for enhancement, and projects back onto GLB. Runs after simplification, before WEARFITS upload. Falls back to original GLB on failure. Enabled by default."},"refineLogo":{"type":"boolean","default":false,"description":"Run a second AI pass focused on fixing logos, brand text, and emblems after texture enhancement. Requires enhanceTexture to be enabled. Works with both 4-view and 6-view modes."},"smoothNormals":{"type":"boolean","default":true,"description":"Apply auto smooth normals (30° angle threshold) to the final GLB before WEARFITS upload. Enabled by default — improves lighting/shading for rendering. **Disable this if the GLB is going to be edited manually afterwards** (for example in Blender): otherwise it can cause problems when using Merge by Distance or sculpting tools. Reason: smoothing splits vertices at sharp edges into coincident duplicates to carry different normals (a glTF limitation — no per-face-corner/loop normals), so Merge by Distance destroys the sharp-edge shading, and sculpting brushes move only one of the duplicates leaving holes."},"genaiQuality":{"type":"string","enum":["default","high"],"default":"high","description":"AI model quality for image correction and texture enhancement. Defaults to \"high\". \"default\" uses 4-view texture enhancement (faster, cheaper). \"high\" uses 6-view texture enhancement for full coverage (4 sides + top + bottom, higher quality, slower). Packshot correction uses the same Gemini 3.1 Flash chain in both. Both have fallback chains for reliability."},"sessionId":{"type":"string","description":"Optional session ID to be passed to WEARFITS as token during shoe upload. Used for compatibility with frontend apps that have user sessions, ensuring uploaded objects are correctly assigned to the user in their WEARFITS account. Takes precedence over the account resolved from the API credential: when a sessionId is sent it becomes the only ownership claim on the upload."},"gapFillReproject":{"type":"boolean","description":"Re-project texture gaps (texels not covered by any rendered view) during AI texture enhancement. Defaults to true whenever enhanceTexture is on (the server can disable the default); send false to opt out. true with enhanceTexture: false is rejected with 400; false is always accepted. The enhanced result and the WEARFITS link are cached per effective option set, so different settings never share a cached texture."},"aoBake":{"type":"string","enum":["baked","off"],"description":"Bake ambient occlusion into the base color texture during AI texture enhancement. Defaults to \"baked\" whenever enhanceTexture is on (the server can disable the default); send \"off\" to opt out. \"baked\" with enhanceTexture: false is rejected with 400; \"off\" is always accepted. Same cache behavior as gapFillReproject."},"aoStrength":{"type":"number","minimum":0,"maximum":1,"description":"Ambient-occlusion strength 0..1 (server default 0.7 when omitted). Ignored when aoBake is off."},"pipelineVariant":{"type":"string","enum":["A","B"],"default":"A","description":"**BETA — opt-in, not recommended for production.** \"A\" (default, production) = standard pipeline: hunyuan-3d generates at 50k triangles, all operations run on 50k, no mesh simplification step. \"B\" = experimental high-poly variant: hunyuan-3d generates at 100k triangles, all operations (rendering, texture projection, AI enhancement, color correction) run on the denser mesh, then the final GLB is simplified to 50k triangles via gltfpack inside v1-compress-glb before auto smooth normals. Initial A/B testing (2026-04-09) showed variant B produces slightly WORSE wireframe topology than variant A due to gltfpack quadric decimation, and the expected texture-sharpness gain from denser projection was marginal. Variant B remains available for future experimentation but should not be used in production until a clear quality win is demonstrated."},"productType":{"type":"string","enum":["shoe","other","auto"],"default":"shoe","description":"\"shoe\" (default): shoe-specific prompts and orientation. \"other\": generic product prompts, no shoe-specific orientation or materials. \"auto\": quick AI detection from the uploaded images — any footwear detected uses shoe pipeline, everything else uses generic."}}},"webhookUrl":{"type":"string","format":"uri","description":"Webhook URL (HTTPS only) to notify when job is complete","example":"https://your-app.com/webhooks/wearfits"}}},"MirrorLogosResponse":{"type":"object","properties":{"success":{"type":"boolean","enum":[true]},"jobId":{"type":"string","format":"uuid","example":"550e8400-e29b-41d4-a716-446655440000"},"status":{"type":"string","enum":["queued"]},"statusUrl":{"type":"string","format":"uri","example":"https://api.wearfits.com/api/v1/jobs/550e8400-e29b-41d4-a716-446655440000"}},"required":["success","jobId","status","statusUrl"]},"MirrorLogosOptions":{"type":"object","properties":{"flipSymmetric":{"type":"boolean","description":"Also repair marks the algorithm judges bilaterally symmetric (flip-invariant, e.g. a trefoil). Default false — symmetric marks are left untouched since mirroring them is a no-op.","example":false}}},"MirrorLogosRequest":{"type":"object","properties":{"glbUrl":{"type":"string","format":"uri","description":"HTTPS URL to the source (right-foot) GLB. The pipeline mirrors the mesh and produces the L/P (left-foot) albedo texture with logos repaired to read correctly on the mirrored foot.","example":"https://api.wearfits.com/files/signed?key=..."},"options":{"$ref":"#/components/schemas/MirrorLogosOptions"}},"required":["glbUrl"]},"ProductDiscoveryClassification":{"type":"object","properties":{"isMatch":{"type":"boolean","description":"The LLM confirmed the images show a product of the requested category"},"sameProduct":{"type":"boolean","description":"All selected images depict the same individual product"},"productType":{"type":"string","description":"Free-form sub-type label from the LLM (e.g. \"sneaker\", \"loafer\", \"top-handle handbag\")","example":"sneaker"},"reason":{"type":"string","description":"Short natural-language justification for the decision"}},"required":["isMatch","sameProduct","productType","reason"]},"ProductDiscoveryNavigationStep":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"kind":{"type":"string","enum":["home","category","product","unknown","agent"],"description":"Page role as the crawler saw it. `agent` entries come from the vision-driven fallback, one per agent step."}},"required":["url","kind"]},"ProductDiscoveryResponse":{"type":"object","properties":{"success":{"type":"boolean","enum":[true]},"category":{"type":"string","enum":["shoes","bags"],"description":"Product category to search for. `shoes` = low-profile shoes (sneakers, loafers, moccasins, sport/running — excludes high heels and boots). `bags` = handheld handbags/purses (excludes backpacks, suitcases, wallets).","example":"shoes"},"pageUrl":{"type":"string","format":"uri","description":"Page where the winning images were found","example":"https://example-shop.com/products/white-sneaker-x"},"images":{"type":"array","items":{"type":"string","format":"uri"},"minItems":1,"maxItems":4,"description":"1–4 image URLs, ordered best-first for packshot-to-3D generation. Feed these directly to POST /api/v1/shoe-3d. Normally 3–4 angles of the same product; falls back to a single packshot when the shop only shows one angle per product (common on small retailers where color variants are separate product pages).","example":["https://cdn.example-shop.com/p/front.jpg","https://cdn.example-shop.com/p/side.jpg","https://cdn.example-shop.com/p/top.jpg"]},"classification":{"$ref":"#/components/schemas/ProductDiscoveryClassification"},"navigationPath":{"type":"array","items":{"$ref":"#/components/schemas/ProductDiscoveryNavigationStep"},"description":"Pages visited during the crawl, first → last. Agent-driven steps use `kind: \"agent\"`."},"resolvedBy":{"type":"string","enum":["fast-path","agent"],"description":"`fast-path` = heuristic crawler + LLM image-set validator. `agent` = vision-driven fallback (takes screenshots and decides match/click/give_up)."}},"required":["success","category","pageUrl","images","classification","navigationPath","resolvedBy"]},"ProductDiscoveryAsyncResponse":{"type":"object","properties":{"success":{"type":"boolean","enum":[true]},"jobId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["queued"]},"statusUrl":{"type":"string","format":"uri","description":"Poll this URL (GET) until status is `completed` or `failed`. On completion the job payload contains `productDiscoveryResult`."},"estimatedProcessingTime":{"type":"integer","description":"Rough estimate in seconds. Typical crawl finishes in 15–90s; 180s is the worst-case ceiling.","example":60}},"required":["success","jobId","status","statusUrl","estimatedProcessingTime"]},"ProductDiscoveryErrorResponse":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"object","properties":{"code":{"type":"string","enum":["NO_PRODUCT_FOUND","DISCOVERY_TIMEOUT","DISCOVERY_FAILED"]},"message":{"type":"string"},"navigationPath":{"type":"array","items":{"$ref":"#/components/schemas/ProductDiscoveryNavigationStep"},"description":"Pages visited before failure — helpful for debugging"}},"required":["code","message"]}},"required":["success","error"]},"ProductDiscoveryRequest":{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"Domain, category page, or product page URL to crawl","example":"https://example-shop.com/shoes/white-sneaker"},"category":{"type":"string","enum":["shoes","bags"],"default":"shoes","description":"Product category to search for. `shoes` = low-profile shoes (sneakers, loafers, moccasins, sport/running — excludes high heels and boots). `bags` = handheld handbags/purses (excludes backpacks, suitcases, wallets).","example":"shoes"},"options":{"type":"object","properties":{"maxNavDepth":{"type":"integer","minimum":0,"maximum":3,"default":2,"description":"How many navigation hops to follow from the input URL (0 = only scan the input page). Default 2: home → category → product.","example":2},"timeoutMs":{"type":"integer","minimum":10000,"maximum":280000,"default":120000,"description":"Per-request wall-clock budget in milliseconds. Hard cap 280s.","example":120000},"async":{"type":"boolean","default":false,"description":"If true, the endpoint returns immediately with a `jobId` and `statusUrl`. The crawl runs on the queue and the result lands on `GET /api/v1/jobs/{jobId}` once complete (field `productDiscoveryResult`). Recommended for clients with short HTTP timeouts (e.g. Google Sheets Apps Script, serverless platforms).","example":false}},"default":{},"description":"Optional tuning knobs for the crawl"}},"required":["url"]},"VirtualFittingResponse":{"type":"object","properties":{"success":{"type":"boolean","enum":[true]},"digitalTwinId":{"type":"string","pattern":"^[a-f0-9]{64}$","description":"Unique identifier for a previously generated digital twin (SHA-256 hex hash)","example":"24e73099501085bae09d26bcdc53624142cac544d5b015c599e498df8a9f6b5e"},"jobId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["queued","completed"]},"estimatedProcessingTime":{"type":"number","description":"Estimated time in seconds"},"statusUrl":{"type":"string","format":"uri"},"mode":{"type":"string","enum":["direct","photo","measurements","clothing_size","twin_id"],"description":"Processing mode used for the request"},"twinCached":{"type":"boolean","description":"Whether the digital twin was retrieved from cache (faster processing)"},"results":{"type":"array","items":{"$ref":"#/components/schemas/JobResult"},"description":"Results (returned immediately if cached)"}},"required":["success","jobId","status","estimatedProcessingTime","statusUrl","mode","twinCached"]},"VirtualFittingRequest":{"type":"object","properties":{"faceImage":{"type":"string","description":"Face/head photo for identity preservation (URL or base64). Required when generating a new twin from silhouetteImage, photoUrl, bodyMeasurements or clothingSize. Not needed when reusing digitalTwinId.","example":"https://example.com/face.jpg"},"silhouetteImage":{"type":"string","description":"[Direct Mode] Pre-rendered body silhouette/depth image (URL or base64), paired with faceImage. Choose this or another body source; faceImage alone does not require this particular mode.","example":"https://example.com/silhouette.png"},"photoUrl":{"type":"string","description":"[Photo Mode] Full-body photo (URL or base64) showing the person head to toe. Pair with faceImage: the worker requires both to generate a new photo-mode twin. A photoUrl-only request can pass schema validation but fails asynchronously with MISSING_INPUTS. poseId is optional.","example":"https://example.com/full-body-photo.jpg"},"poseId":{"type":"string","enum":["standing_arms_down","man_pose","girl_pose","shoe_girl_pose","walking_pose","default"],"default":"default","description":"Pose used when generating a new twin from photoUrl, bodyMeasurements or clothingSize. Ignored for an existing digitalTwinId or pre-rendered silhouette. To change an existing twin pose, create a new twin. The default pose is resolved from deployment configuration and gender.","example":"default"},"bodyMeasurements":{"allOf":[{"$ref":"#/components/schemas/BodyMeasurements"},{"description":"**[Measurements Mode]** Generate body silhouette from measurements instead of a photo.\n\n**What to provide:** At least 2 body measurements in centimeters:\n- height: Body height (140-210 cm)\n- chest: Chest/bust circumference (70-160 cm)\n- waist: Waist circumference (55-170 cm)\n- hip: Hip circumference (70-170 cm)\n- inseam: Inseam length (60-100 cm)\n\nThese are validation bounds, not the range the body dataset can represent. Matching is\nnearest-neighbour against a reference body dataset that never rejects a query, so a value\ninside the bounds but outside the data is served by extrapolation to the closest available\nbody. The dataset's real spread is per gender, and much narrower than the bounds:\n\n- female (n=800): height 140.9-184.5, chest 77.1-153.9, waist 64.1-147.0, hip 80.7-158.5, inseam 62.7-89.5 cm\n- male (n=1218): height 154.6-197.5, chest 82.3-152.4, waist 66.4-164.3, hip 83.6-166.9, inseam 66.7-94.5 cm\n\nThe gender field is optional. Omitting it widens the searched population to the union of both\ngenders, so a value that would be extrapolation for one gender can come back in range\nbecause a subject of the other gender matched it.\n\n**What happens:** The API:\n1. Finds the closest matching body from our dataset\n2. Generates a 3D mesh with the matched body shape\n3. Applies the specified pose\n4. Creates the digital twin\n5. Applies garments to the model\n\n**Use this if:** You have body measurement data (e.g., from a sizing profile) but no full-body photo.\n\n**Note:** Results are approximations based on nearest-neighbor matching."}]},"clothingSize":{"allOf":[{"$ref":"#/components/schemas/ClothingSize"},{"description":"**[Clothing Size Mode]** Generate body from clothing size instead of exact measurements.\n\n**What to provide:**\n- height: Body height in cm (140-210)\n- size: Clothing size (XS, S, M, L, XL, XXL, 3XL)\n\n**What happens:** The API:\n1. Converts size to body measurements using market-average data from major brands\n2. Finds the closest matching body from our dataset\n3. Generates a 3D mesh with the matched body shape\n4. Creates the digital twin\n5. Applies garments to the model\n\n**Use this if:** You only know the user's clothing size, not their exact measurements.\n\n**Note:** Requires `gender` field to be set (defaults to female if not provided).\nMarket averages from: H&M, Uniqlo, ASOS, Nike, Adidas, Zara, Gap, Shein, Boohoo, Lululemon."}]},"digitalTwinId":{"type":"string","pattern":"^[a-f0-9]{64}$","description":"Unique identifier for a previously generated digital twin (SHA-256 hex hash)","example":"24e73099501085bae09d26bcdc53624142cac544d5b015c599e498df8a9f6b5e"},"gender":{"type":"string","enum":["male","female","neutral"],"description":"Gender for prompt building. If not provided, uses neutral pronoun.","example":"female"},"topGarment":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":2,"description":"Array of 1-2 images (URLs or base64): first is packshot, second (optional) is on-model reference","example":["https://example.com/packshot.jpg","https://example.com/on-model.jpg"]}],"description":"Top garment (shirt, blouse, etc.) - single URL or array of [packshot, on-model-reference]","example":["https://example.com/top-packshot.jpg","https://example.com/top-on-model.jpg"]},"bottomGarment":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":2,"description":"Array of 1-2 images (URLs or base64): first is packshot, second (optional) is on-model reference","example":["https://example.com/packshot.jpg","https://example.com/on-model.jpg"]}],"description":"Bottom garment (pants, skirt, etc.) - single URL or array of [packshot, on-model-reference]","example":"https://example.com/bottom.jpg"},"fullBodyGarment":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":2,"description":"Array of 1-2 images (URLs or base64): first is packshot, second (optional) is on-model reference","example":["https://example.com/packshot.jpg","https://example.com/on-model.jpg"]}],"description":"Full-body garment (dress, jumpsuit, etc.) - single URL or array of [packshot, on-model-reference]","example":["https://example.com/dress-packshot.jpg","https://example.com/dress-on-model.jpg"]},"shoes":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":2,"description":"Array of 1-2 images (URLs or base64): first is packshot, second (optional) is on-model reference","example":["https://example.com/packshot.jpg","https://example.com/on-model.jpg"]}],"description":"Optional shoes to add to any outfit - single URL or array of [packshot, on-model-reference]","example":"https://example.com/heels.jpg"},"productImage":{"type":"string","description":"Single product image shortcut (explicit opt-in for direct 2-image path). When provided (and no slot garments), the image model receives exactly the person/digital twin + this one product image (no garment grid composed; uses dedicated single-product prompt). Use productCategory for hint.","example":"https://example.com/product.jpg"},"productCategory":{"type":"string","enum":["auto","top","bottom","full-body","shoe"],"default":"auto","description":"Category for productImage. Use `auto` to let the image model infer what wearable product is shown.","example":"auto"},"prompt":{"type":"string","minLength":1,"maxLength":2000,"description":"Optional additional styling prompt. Will be combined with default digital twin prompt. Use this to customize clothing, background, etc.","example":"wearing casual jeans and white t-shirt, outdoor setting"},"options":{"type":"object","properties":{"quality":{"type":"string","enum":["draft","standard","high"],"default":"standard","description":"Quality tier affects processing time"},"seed":{"type":"integer","description":"Random seed for reproducible generation (not guaranteed with all models)"},"skipCache":{"type":"boolean","default":false,"description":"If true, skip digital twin cache and always generate fresh"},"skipResultCache":{"type":"boolean","default":false,"description":"If true, skip final result cache and always generate fresh try-on"},"preservePose":{"type":"boolean","default":false,"description":"[Photo Mode only] If true, preserve the original pose from photoUrl instead of applying a standard pose."},"provider":{"type":"string","enum":["openrouter","openai"],"description":"Image provider override for both digital twin and try-on steps."}},"description":"Generation options"},"webhookUrl":{"type":"string","format":"uri","description":"Webhook URL (HTTPS only) to notify when job is complete","example":"https://your-app.com/webhooks/wearfits"}}},"StorefrontTokenResponse":{"type":"object","properties":{"success":{"type":"boolean","enum":[true]},"token":{"type":"string"},"audience":{"type":"string","enum":["storefront","account","usage"]},"expiresAt":{"type":"string","format":"date-time"}},"required":["success","token","audience","expiresAt"]},"StorefrontTokenRequest":{"type":"object","properties":{"audience":{"type":"string","enum":["storefront","account","usage"],"description":"`storefront` is the browser widget token (`wfs1.`). `account` is the server-to-server attribution token (`wfa1.`) sent as `X-Wearfits-Account` next to a service key. `usage` (`wfu1.`, minted only with the `USAGE_TOKEN_HANDSHAKE` secret as `handshake`, at most 2 hours) is analytics-only attribution sent as `X-Wearfits-Usage-Account`: it grants no quota, ownership or access.","example":"storefront"},"ttlSeconds":{"type":"integer","minimum":1,"maximum":86400,"description":"Lifetime in seconds. Defaults to 7200 for storefront and 3600 for account. Capped at 86400.","example":7200},"handshake":{"type":"string","minLength":1,"description":"Server-to-server bootstrap: the `DEV_INTEGRATION_HANDSHAKE` secret (for `audience: \"usage\"`: the separate `USAGE_TOKEN_HANDSHAKE` secret, which mints nothing else). Required when minting without a resolved merchant account (no merchant key / no `X-Wearfits-Account`)."},"userId":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,64}$","description":"SaaS `user_id` to mint for. Required with `handshake`. Ignored when the caller already authenticated as that merchant.","example":"clxxxxxxxxxxxxxxxxxxxx"}}},"SizeFittingResponse":{"type":"object","properties":{"success":{"type":"boolean","enum":[true]},"available":{"type":"boolean"},"reason":{"type":"string","enum":["body_profile_unavailable","profile_quality_too_low"]},"confidence":{"type":"string","enum":["estimated"]},"recommendations":{"type":"array","items":{"type":"object","properties":{"productId":{"type":"string"},"category":{"type":"string","enum":["top","bottom","fullBody"]},"recommendedSize":{"type":"string"},"sizes":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string"},"dimensions":{"type":"object","properties":{"height":{"type":"object","properties":{"status":{"type":"string","enum":["fit","loose","tight"]},"score":{"type":"number","minimum":-5,"maximum":5}},"required":["status","score"]},"chest":{"type":"object","properties":{"status":{"type":"string","enum":["fit","loose","tight"]},"score":{"type":"number","minimum":-5,"maximum":5}},"required":["status","score"]},"waist":{"type":"object","properties":{"status":{"type":"string","enum":["fit","loose","tight"]},"score":{"type":"number","minimum":-5,"maximum":5}},"required":["status","score"]},"hip":{"type":"object","properties":{"status":{"type":"string","enum":["fit","loose","tight"]},"score":{"type":"number","minimum":-5,"maximum":5}},"required":["status","score"]},"inseam":{"type":"object","properties":{"status":{"type":"string","enum":["fit","loose","tight"]},"score":{"type":"number","minimum":-5,"maximum":5}},"required":["status","score"]}}}},"required":["label","dimensions"]}}},"required":["productId","category","recommendedSize","sizes"]}}},"required":["success","available","recommendations"],"example":{"success":true,"available":true,"confidence":"estimated","recommendations":[{"productId":"shirt-123","category":"top","recommendedSize":"M","sizes":[{"label":"S","dimensions":{"chest":{"status":"tight","score":-1.38},"waist":{"status":"tight","score":-1.25}}},{"label":"M","dimensions":{"chest":{"status":"fit","score":0.75},"waist":{"status":"fit","score":0.5}}}]}]}},"SizeFittingRequest":{"type":"object","properties":{"digitalTwinId":{"type":"string","pattern":"^[a-f0-9]{64}$"},"products":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":128},"category":{"type":"string","enum":["top","bottom","fullBody"]},"sizeChart":{"type":"object","properties":{"version":{"type":"number","enum":[1]},"basis":{"type":"string","enum":["body"]},"unit":{"type":"string","enum":["cm","in"]},"sizes":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":32},"measurements":{"type":"object","properties":{"height":{"type":"object","properties":{"min":{"type":"number","minimum":1,"maximum":500},"max":{"type":"number","minimum":1,"maximum":500}},"required":["min","max"]},"chest":{"type":"object","properties":{"min":{"type":"number","minimum":1,"maximum":500},"max":{"type":"number","minimum":1,"maximum":500}},"required":["min","max"]},"waist":{"type":"object","properties":{"min":{"type":"number","minimum":1,"maximum":500},"max":{"type":"number","minimum":1,"maximum":500}},"required":["min","max"]},"hip":{"type":"object","properties":{"min":{"type":"number","minimum":1,"maximum":500},"max":{"type":"number","minimum":1,"maximum":500}},"required":["min","max"]},"inseam":{"type":"object","properties":{"min":{"type":"number","minimum":1,"maximum":500},"max":{"type":"number","minimum":1,"maximum":500}},"required":["min","max"]}},"additionalProperties":false}},"required":["label","measurements"],"additionalProperties":false},"minItems":1,"maxItems":30}},"required":["version","basis","unit","sizes"],"additionalProperties":false}},"required":["id","category","sizeChart"],"additionalProperties":false},"minItems":1,"maxItems":20}},"required":["digitalTwinId","products"],"additionalProperties":false,"example":{"digitalTwinId":"c3721f86f03c1d7035f0728cad2ca97d027781dc7e1722cf92901e10f56007c0","products":[{"id":"shirt-123","category":"top","sizeChart":{"version":1,"basis":"body","unit":"cm","sizes":[{"label":"S","measurements":{"chest":{"min":86,"max":94},"waist":{"min":70,"max":78}}},{"label":"M","measurements":{"chest":{"min":94,"max":102},"waist":{"min":78,"max":86}}}]}}]}},"PoseTransferResponse":{"type":"object","properties":{"success":{"type":"boolean","enum":[true]},"jobId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["queued","completed"]},"estimatedProcessingTime":{"type":"number","description":"Estimated time in seconds"},"statusUrl":{"type":"string","format":"uri"},"mode":{"type":"string","enum":["cached_pose","image_pose"],"description":"Processing mode: cached_pose (using poseId) or image_pose (extracting from poseImageUrl)"},"digitalTwinId":{"type":"string","description":"Unique ID for the generated digital twin (returned immediately if cached)"}},"required":["success","jobId","status","estimatedProcessingTime","statusUrl","mode"]},"PoseTransferRequest":{"type":"object","properties":{"sourceImageUrl":{"type":"string","format":"uri","description":"URL of the source person's full-body photo. This person's body shape and face will be preserved in the output.\n\n**Requirements:**\n- Must show FULL BODY (head to feet visible)\n- Person should be standing\n- Good lighting, minimal obstructions","example":"https://example.com/person-a-fullbody.jpg"},"poseImageUrl":{"type":"string","format":"uri","description":"URL of a reference person's photo whose pose you want to copy.\n\n**Requirements:**\n- Must show FULL BODY (head to feet visible)\n- Clear pose visible\n- Can be any person - only the pose is extracted\n\n**Processing:** Adds ~20s for pose extraction via SAM-3D.","example":"https://example.com/person-b-pose-reference.jpg"},"poseId":{"type":"string","enum":["standing_arms_down","man_pose","girl_pose","shoe_girl_pose","walking_pose","default"],"description":"Pre-defined pose ID to apply. Faster than poseImageUrl since no extraction needed.\n\n**Available poses:**\n- `default`: System default pose (currently girl_pose, configurable)\n- `standing_arms_down`: Natural standing pose with arms relaxed at sides\n- `man_pose`: Male standing pose\n- `girl_pose`: Female standing pose\n- `shoe_girl_pose`: Pose optimized for shoe try-on","example":"default"},"gender":{"type":"string","enum":["male","female","neutral"],"description":"Gender for prompt building when generating the final image. If not provided, uses neutral.","example":"female"},"options":{"type":"object","properties":{"quality":{"type":"string","enum":["draft","standard","high"],"default":"standard","description":"Quality tier affects processing time and output resolution"},"seed":{"type":"integer","description":"Random seed for reproducible generation (not guaranteed with all models)"},"skipCache":{"type":"boolean","default":false,"description":"If true, skip cache and always generate fresh result"},"includeGlb":{"type":"boolean","default":true,"description":"If true, include the transformed GLB file in results"},"includeVisualization":{"type":"boolean","default":true,"description":"If true, include a mesh visualization PNG in results"}},"description":"Generation options"},"webhookUrl":{"type":"string","format":"uri","description":"Webhook URL (HTTPS only) to notify when job is complete","example":"https://your-app.com/webhooks/wearfits"}},"required":["sourceImageUrl"]},"WearFitsTryOn":{"type":"object","properties":{"modelId":{"type":"string","description":"WEARFITS model ID"},"colorId":{"type":"string","description":"WEARFITS color ID"},"viewerUrl":{"type":"string","format":"uri","description":"URL to the WEARFITS AR try-on viewer"},"glbUrl":{"type":"string","format":"uri","description":"URL to the WEARFITS-hosted GLB after autofit positioning (fitting-room-ready, carries embedded fit metadata). Distinct from the generation GLB in `results[].url`. Present only for shoe uploads once autofit reports \"finished\"."}},"required":["modelId","colorId","viewerUrl"]},"Shoe3DModelResult":{"type":"object","properties":{"model":{"type":"string","description":"Identifier of the model that generated this result"},"status":{"type":"string","enum":["queued","processing","completed","failed"],"description":"Status of this specific model generation"},"glbUrl":{"type":"string","format":"uri","description":"URL to the generated GLB 3D model file"},"zipUrl":{"type":"string","format":"uri","description":"URL to the generated ZIP archive (for models that output zip)"},"thumbnailUrl":{"type":"string","format":"uri","description":"URL to a thumbnail preview image"},"renderGridUrl":{"type":"string","format":"uri","description":"6-angle preview grid image URL for quality inspection"},"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"],"description":"Error details if generation failed"},"timeMs":{"type":"number","description":"Generation time in milliseconds"},"providerJobId":{"type":"string","description":"Provider-specific job ID for tracking"},"statusUrl":{"type":"string","description":"Provider status URL for checking generation progress"},"enhancedGlbUrl":{"type":"string","format":"uri","description":"URL to AI texture-enhanced GLB (if enhanceTexture was enabled)"},"textureEnhancementError":{"type":"string","description":"Error message if texture enhancement failed (original GLB used as fallback)"},"logoGridOutputUrl":{"type":"string","format":"uri","description":"URL to logo-refined grid image (if refineLogo was enabled)"},"logoModelUsed":{"type":"string","description":"AI model used for logo refinement pass"}},"required":["model","status"]},"MirrorLogosOutput":{"type":"object","properties":{"textureUrl":{"type":"string","format":"uri","description":"Signed HTTPS URL to the repaired L/P albedo texture (PNG)"},"report":{"type":"object","additionalProperties":{"nullable":true},"description":"Pipeline report: clusters, unrepaired marks, version, optional warning"}},"required":["textureUrl","report"],"description":"Result for mirror-logos jobs: the L/P (left-foot) albedo texture URL and the pipeline report. Populated once the job reaches status=completed."},"JobError":{"type":"object","properties":{"code":{"type":"string","example":"PROVIDER_ERROR"},"message":{"type":"string","example":"External provider returned an error"},"retryable":{"type":"boolean","example":true}},"required":["code","message","retryable"]},"JobProgress":{"type":"object","properties":{"stage":{"type":"string","example":"processing"},"percentage":{"type":"number","minimum":0,"maximum":100,"description":"Absent for virtual-fitting/digital-twin until completed (100). Render indeterminate progress when absent.","example":50}},"required":["stage"]},"JobStatusResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["queued","validating","processing","uploading","completed","failed"]},"mode":{"type":"string","enum":["clothing","shoes","generate","generate-3d","shoe-3d","digital-twin","virtual-fitting","pose-transfer","product-discovery","mirror-logos"]},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"results":{"type":"array","items":{"$ref":"#/components/schemas/JobResult"}},"digitalTwinId":{"type":"string","pattern":"^[a-f0-9]{64}$","description":"ID of the generated or reused digital twin (SHA-256 hex)"},"wearfitsTryOn":{"$ref":"#/components/schemas/WearFitsTryOn"},"wearfitsError":{"type":"string"},"shoe3dModelResults":{"type":"array","items":{"$ref":"#/components/schemas/Shoe3DModelResult"},"description":"Per-model results for shoe-3d jobs (includes enhancedGlbUrl if texture enhancement was enabled)"},"productDiscoveryResult":{"type":"object","properties":{"category":{"type":"string","enum":["shoes","bags"]},"pageUrl":{"type":"string","format":"uri"},"images":{"type":"array","items":{"type":"string","format":"uri"},"minItems":1,"maxItems":4},"classification":{"type":"object","properties":{"isMatch":{"type":"boolean"},"sameProduct":{"type":"boolean"},"productType":{"type":"string"},"reason":{"type":"string"}},"required":["isMatch","sameProduct","productType","reason"]},"navigationPath":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string"},"kind":{"type":"string","enum":["home","category","product","unknown","agent"]}},"required":["url","kind"]}},"resolvedBy":{"type":"string","enum":["fast-path","agent"]}},"required":["category","pageUrl","images","classification","navigationPath","resolvedBy"],"description":"Final result for async product-discovery jobs. Populated once the job reaches status=completed."},"output":{"$ref":"#/components/schemas/MirrorLogosOutput"},"error":{"$ref":"#/components/schemas/JobError"},"progress":{"$ref":"#/components/schemas/JobProgress"}},"required":["id","status","mode","createdAt","updatedAt"]},"SuccessResponse":{"type":"object","properties":{"success":{"type":"boolean","enum":[true]}},"required":["success"]},"TraceAttempt":{"type":"object","properties":{"attempt":{"type":"integer","minimum":0,"exclusiveMinimum":true,"description":"Attempt number (1-indexed)","example":1},"durationMs":{"type":"integer","minimum":0,"description":"Duration of this attempt in milliseconds","example":2500},"success":{"type":"boolean","description":"Whether this attempt succeeded","example":true},"error":{"type":"string","description":"Error message if attempt failed","example":"fetch failed"}},"required":["attempt","durationMs","success"]},"TraceEntry":{"type":"object","properties":{"id":{"type":"string","description":"Unique operation ID within this trace","example":"op_1"},"operation":{"type":"string","description":"Operation name","example":"image_validation"},"startedAt":{"type":"string","format":"date-time","description":"ISO 8601 timestamp when operation started","example":"2024-01-15T12:00:00.100Z"},"endedAt":{"type":"string","format":"date-time","description":"ISO 8601 timestamp when operation ended","example":"2024-01-15T12:00:03.500Z"},"durationMs":{"type":"integer","minimum":0,"description":"Total duration in milliseconds","example":3400},"status":{"type":"string","enum":["started","success","failed","skipped"],"description":"Operation status","example":"success"},"input":{"type":"object","additionalProperties":{"nullable":true},"description":"Sanitized input parameters","example":{"imageCount":2,"correctImage":true}},"output":{"type":"object","additionalProperties":{"nullable":true},"description":"Sanitized output (URLs only, no data)","example":{"isValid":true,"bestIndex":0}},"error":{"type":"object","properties":{"code":{"type":"string","example":"TIMEOUT"},"message":{"type":"string","example":"Operation timed out after 180000ms"}},"required":["code","message"],"description":"Error details if operation failed"},"attempts":{"type":"array","items":{"$ref":"#/components/schemas/TraceAttempt"},"description":"Individual retry attempts"}},"required":["id","operation","startedAt","status"]},"TraceSummary":{"type":"object","properties":{"totalOperations":{"type":"integer","minimum":0,"description":"Total number of operations recorded","example":12},"successCount":{"type":"integer","minimum":0,"description":"Number of successful operations","example":11},"failureCount":{"type":"integer","minimum":0,"description":"Number of failed operations","example":1},"skippedCount":{"type":"integer","minimum":0,"description":"Number of skipped operations","example":0},"totalRetries":{"type":"integer","minimum":0,"description":"Total number of retry attempts across all operations","example":2}},"required":["totalOperations","successCount","failureCount","skippedCount","totalRetries"],"description":"Summary statistics"},"TraceLogResponse":{"type":"object","properties":{"jobId":{"type":"string","format":"uuid","description":"Job ID this trace belongs to","example":"550e8400-e29b-41d4-a716-446655440000"},"startedAt":{"type":"string","format":"date-time","description":"ISO 8601 timestamp when processing started","example":"2024-01-15T12:00:00.000Z"},"completedAt":{"type":"string","format":"date-time","description":"ISO 8601 timestamp when processing completed","example":"2024-01-15T12:02:35.000Z"},"totalDurationMs":{"type":"integer","minimum":0,"description":"Total processing duration in milliseconds","example":155000},"status":{"type":"string","enum":["processing","completed","failed"],"description":"Overall trace status","example":"completed"},"entries":{"type":"array","items":{"$ref":"#/components/schemas/TraceEntry"},"description":"Ordered list of operation entries"},"summary":{"$ref":"#/components/schemas/TraceSummary"}},"required":["jobId","startedAt","status","entries","summary"]},"TextureEnhanceResponse":{"type":"object","properties":{"success":{"type":"boolean","enum":[true]},"imageUrl":{"type":"string","description":"Enhanced texture image as base64 data URL"}},"required":["success","imageUrl"]},"TextureEnhanceRequest":{"type":"object","properties":{"renderImage":{"type":"string","description":"Current texture render as base64 data URL"},"referenceImages":{"type":"array","items":{"type":"string","description":"Image as base64 data URL (data:image/png;base64,...), max 10 MB"},"minItems":1,"maxItems":8,"description":"Reference packshot images (1-8) as base64 data URLs"},"mode":{"type":"string","enum":["single","grid"],"default":"single","description":"Enhancement mode: single view or 2x2 grid of 4 views"},"model":{"type":"string","description":"OpenRouter model ID to use for enhancement."}},"required":["renderImage","referenceImages"]}},"parameters":{}},"paths":{"/health":{"get":{"tags":["Health"],"summary":"Health check","description":"Returns the health status of the API","security":[],"responses":{"200":{"description":"Service is healthy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}}}}},"/health/warmup":{"get":{"tags":["Health"],"summary":"Warm up all external services","description":"Warms up Modal workers and Cloudflare queue consumer. Call this when a user enters the try-on flow to reduce cold start latency. Both warmups run in parallel.","security":[],"responses":{"200":{"description":"Warmup completed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WarmupResponse"}}}}}}},"/health/queue-warmup":{"get":{"tags":["Health"],"summary":"Warm up queue consumer only (use /warmup instead)","description":"Enqueues a lightweight message to keep the queue consumer warm. **Prefer using /health/warmup** which warms both Modal and queue in a single call.","security":[],"responses":{"200":{"description":"Queue warmup message enqueued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueueWarmupResponse"}}}}}}},"/files/signed":{"get":{"tags":["Files"],"summary":"Get a signed file","description":"Retrieve a file using a signed URL. No API key required for valid signed URLs.","security":[],"parameters":[{"schema":{"type":"string","description":"File key (path)"},"required":true,"name":"key","in":"query"},{"schema":{"type":"string","description":"HMAC token"},"required":true,"name":"token","in":"query"},{"schema":{"type":"string","description":"Expiry timestamp"},"required":true,"name":"expires","in":"query"}],"responses":{"200":{"description":"File content","content":{"image/png":{"schema":{"nullable":true}},"image/jpeg":{"schema":{"nullable":true}}}},"403":{"description":"Invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"File not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/files/{key}":{"get":{"tags":["Files"],"summary":"Get a result file","description":"Retrieve a generated result file from storage. Either provide the same X-API-Key used to create the job, or provide both signed query parameters (`token` and `expires`). Files expire after 24 hours.","security":[{"apiKey":[]},{}],"parameters":[{"schema":{"type":"string","description":"File key (path)"},"required":true,"name":"key","in":"path"},{"schema":{"type":"string","description":"HMAC token for signed-URL access (use together with expires)"},"required":false,"name":"token","in":"query"},{"schema":{"type":"string","description":"Signed-URL expiry timestamp in milliseconds (use together with token)"},"required":false,"name":"expires","in":"query"}],"responses":{"200":{"description":"File content","content":{"image/png":{"schema":{"nullable":true}},"image/jpeg":{"schema":{"nullable":true}},"image/webp":{"schema":{"nullable":true}},"model/gltf-binary":{"schema":{"nullable":true}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Access denied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"File not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/files/results/{jobId}/{filename}":{"get":{"tags":["Files"],"summary":"Get a result file by job ID","description":"Retrieve a generated result file from storage using job ID and filename. Either provide the same X-API-Key used to create the job, or provide both signed query parameters (`token` and `expires`).","security":[{"apiKey":[]},{}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"Job ID"},"required":true,"name":"jobId","in":"path"},{"schema":{"type":"string","description":"Filename"},"required":true,"name":"filename","in":"path"},{"schema":{"type":"string","description":"HMAC token for signed-URL access (use together with expires)"},"required":false,"name":"token","in":"query"},{"schema":{"type":"string","description":"Signed-URL expiry timestamp in milliseconds (use together with token)"},"required":false,"name":"expires","in":"query"}],"responses":{"200":{"description":"File content","content":{"image/png":{"schema":{"nullable":true}},"image/jpeg":{"schema":{"nullable":true}},"image/webp":{"schema":{"nullable":true}},"model/gltf-binary":{"schema":{"nullable":true}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Access denied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"File not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/monitoring/api/data":{"get":{"tags":["Monitoring"],"security":[],"summary":"Get monitoring data","description":"Returns recent digital twin and try-on generation data for the dashboard","parameters":[{"schema":{"type":"string","enum":["twins","tryons","pose-transfers","shoe3ds"],"description":"Optional tab filter to limit results","example":"tryons"},"required":false,"name":"tab","in":"query"}],"responses":{"200":{"description":"Monitoring data","content":{"application/json":{"schema":{"type":"object","properties":{"digitalTwins":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string","enum":["digital-twin"]},"timestamp":{"type":"string"},"mode":{"type":"string","enum":["photo","direct","measurements","clothing_size"]},"status":{"type":"string","enum":["queued","processing","completed","failed"]},"digitalTwinId":{"type":"string"},"timings":{"type":"object","properties":{"queueMs":{"type":"number"},"sam3dMs":{"type":"number"},"poseMs":{"type":"number"},"twinMs":{"type":"number"},"uploadMs":{"type":"number"},"totalMs":{"type":"number"},"measurementsMs":{"type":"number"}}},"inputs":{"type":"object","properties":{"faceImage":{"type":"string"},"bodyPhoto":{"type":"string"},"silhouette":{"type":"string"}},"required":["faceImage"]},"outputs":{"type":"array","items":{"type":"object","properties":{"model":{"type":"string","description":"The model that actually produced this output (may differ from `requestedModel` after an automatic shoe-3d fallback)."},"requestedModel":{"type":"string","description":"The model the client originally requested. Present on shoe-3d entries submitted after the fallback feature shipped."},"fallbackAttempts":{"type":"array","items":{"type":"object","properties":{"model":{"type":"string"},"chainIndex":{"type":"integer","minimum":0},"reason":{"type":"string"}},"required":["model","chainIndex","reason"]},"description":"Per-attempt audit when the fallback chain advanced (failed model + reason). Empty/undefined for jobs that completed on the primary."},"provider":{"type":"string"},"textureGenModel":{"type":"string"},"status":{"type":"string","enum":["completed","failed","processing","queued"]},"resultUrl":{"type":"string"},"renderGridUrl":{"type":"string"},"timeMs":{"type":"number"},"error":{"type":"string"},"fileSize":{"type":"number"},"meshStats":{"type":"object","properties":{"vertices":{"type":"number"},"triangles":{"type":"number"},"fileSizeBytes":{"type":"number"}},"required":["vertices","triangles","fileSizeBytes"]},"simplifiedStats":{"type":"object","properties":{"vertices":{"type":"number"},"triangles":{"type":"number"},"fileSizeBytes":{"type":"number"}},"required":["vertices","triangles","fileSizeBytes"]},"simplifiedGlbUrl":{"type":"string"},"textureGridInputUrl":{"type":"string"},"textureGridOutputUrl":{"type":"string"},"enhancedGlbUrl":{"type":"string"},"wearfitsGlbSize":{"type":"number"},"logoGridOutputUrl":{"type":"string"},"logoModelUsed":{"type":"string"},"logoRefineError":{"type":"string"},"logoRefineProcessing":{"type":"boolean"},"numViews":{"type":"number"},"textureRegenerationId":{"type":"string"},"textureRegeneratedFrom":{"type":"integer","minimum":0},"textureRegenerationRequestedAt":{"type":"string"},"textureRegenerationDispatchedAt":{"type":"string"},"textureOptions":{"type":"object","properties":{"gapFillReproject":{"type":"boolean"},"aoBake":{"type":"string","enum":["baked","off"]},"aoStrength":{"type":"number","minimum":0,"maximum":1},"source":{"type":"object","properties":{"gapFillReproject":{"type":"string","enum":["default","explicit"]},"aoBake":{"type":"string","enum":["default","explicit"]}},"required":["gapFillReproject","aoBake"]}},"description":"Effective texture options of this texture regeneration variant (absent on legacy variants = none)."},"textureOptionsKey":{"type":"string","description":"Cache key of the texture options that actually produced this output (`none` = gap fill and AO off)."},"textureOptionsFallback":{"type":"boolean","description":"True when enhancement with default-on texture options failed and was re-run without them."},"textureOptionsFallbackErrorCode":{"type":"string","description":"Error code of the failed default-options attempt (with textureOptionsFallback or textureOptionsFallbackSkipped)."},"textureOptionsFallbackSkipped":{"type":"string","enum":["deadline"],"description":"The default-options attempt failed but the fallback was skipped because stage 2 was past its deadline."},"gapFill":{"type":"object","additionalProperties":{"nullable":true},"description":"Gap-fill re-projection stats from Modal. Absent = not requested (or legacy entry)."},"aoBake":{"type":"object","additionalProperties":{"nullable":true},"description":"Ambient-occlusion bake stats from Modal (mode/strength/timings, possibly error/reason). Absent = not requested."},"arTryonError":{"type":"string"},"wearfitsTryOn":{"type":"object","properties":{"modelId":{"type":"string"},"colorId":{"type":"string"},"viewerUrl":{"type":"string"},"glbUrl":{"type":"string","description":"The GLB WEARFITS serves for this model after autofit — i.e. the file the viewer actually renders, re-exported on their side. Distinct from `enhancedGlbUrl` (what we uploaded). Absent on legacy entries and whenever autofit did not report a GLB."}},"required":["modelId","colorId","viewerUrl"]}},"required":["model","provider","status","timeMs"]}},"finalResult":{"type":"string"}},"required":["id","type","timestamp","mode","inputs","outputs"]}},"tryons":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string","enum":["tryon"]},"timestamp":{"type":"string"},"mode":{"type":"string","enum":["clothing","shoes"]},"status":{"type":"string","enum":["queued","processing","completed","failed"]},"timings":{"type":"object","properties":{"queueMs":{"type":"number"},"validationMs":{"type":"number"},"gridMs":{"type":"number"},"providerMs":{"type":"number"},"uploadMs":{"type":"number"},"totalMs":{"type":"number"},"twinMs":{"type":"number"},"tryonMs":{"type":"number"},"readyMs":{"type":"number"}}},"inputs":{"type":"object","properties":{"avatar":{"type":"string"},"digitalTwinId":{"type":"string"},"garments":{"type":"array","items":{"type":"string"}},"garmentGrid":{"type":"string"},"poseImage":{"type":"string","description":"Optional pose reference image (OpenPose skeleton) for guided generation"}},"required":["avatar","garments"]},"outputs":{"type":"array","items":{"type":"object","properties":{"model":{"type":"string","description":"The model that actually produced this output (may differ from `requestedModel` after an automatic shoe-3d fallback)."},"requestedModel":{"type":"string","description":"The model the client originally requested. Present on shoe-3d entries submitted after the fallback feature shipped."},"fallbackAttempts":{"type":"array","items":{"type":"object","properties":{"model":{"type":"string"},"chainIndex":{"type":"integer","minimum":0},"reason":{"type":"string"}},"required":["model","chainIndex","reason"]},"description":"Per-attempt audit when the fallback chain advanced (failed model + reason). Empty/undefined for jobs that completed on the primary."},"provider":{"type":"string"},"textureGenModel":{"type":"string"},"status":{"type":"string","enum":["completed","failed","processing","queued"]},"resultUrl":{"type":"string"},"renderGridUrl":{"type":"string"},"timeMs":{"type":"number"},"error":{"type":"string"},"fileSize":{"type":"number"},"meshStats":{"type":"object","properties":{"vertices":{"type":"number"},"triangles":{"type":"number"},"fileSizeBytes":{"type":"number"}},"required":["vertices","triangles","fileSizeBytes"]},"simplifiedStats":{"type":"object","properties":{"vertices":{"type":"number"},"triangles":{"type":"number"},"fileSizeBytes":{"type":"number"}},"required":["vertices","triangles","fileSizeBytes"]},"simplifiedGlbUrl":{"type":"string"},"textureGridInputUrl":{"type":"string"},"textureGridOutputUrl":{"type":"string"},"enhancedGlbUrl":{"type":"string"},"wearfitsGlbSize":{"type":"number"},"logoGridOutputUrl":{"type":"string"},"logoModelUsed":{"type":"string"},"logoRefineError":{"type":"string"},"logoRefineProcessing":{"type":"boolean"},"numViews":{"type":"number"},"textureRegenerationId":{"type":"string"},"textureRegeneratedFrom":{"type":"integer","minimum":0},"textureRegenerationRequestedAt":{"type":"string"},"textureRegenerationDispatchedAt":{"type":"string"},"textureOptions":{"type":"object","properties":{"gapFillReproject":{"type":"boolean"},"aoBake":{"type":"string","enum":["baked","off"]},"aoStrength":{"type":"number","minimum":0,"maximum":1},"source":{"type":"object","properties":{"gapFillReproject":{"type":"string","enum":["default","explicit"]},"aoBake":{"type":"string","enum":["default","explicit"]}},"required":["gapFillReproject","aoBake"]}},"description":"Effective texture options of this texture regeneration variant (absent on legacy variants = none)."},"textureOptionsKey":{"type":"string","description":"Cache key of the texture options that actually produced this output (`none` = gap fill and AO off)."},"textureOptionsFallback":{"type":"boolean","description":"True when enhancement with default-on texture options failed and was re-run without them."},"textureOptionsFallbackErrorCode":{"type":"string","description":"Error code of the failed default-options attempt (with textureOptionsFallback or textureOptionsFallbackSkipped)."},"textureOptionsFallbackSkipped":{"type":"string","enum":["deadline"],"description":"The default-options attempt failed but the fallback was skipped because stage 2 was past its deadline."},"gapFill":{"type":"object","additionalProperties":{"nullable":true},"description":"Gap-fill re-projection stats from Modal. Absent = not requested (or legacy entry)."},"aoBake":{"type":"object","additionalProperties":{"nullable":true},"description":"Ambient-occlusion bake stats from Modal (mode/strength/timings, possibly error/reason). Absent = not requested."},"arTryonError":{"type":"string"},"wearfitsTryOn":{"type":"object","properties":{"modelId":{"type":"string"},"colorId":{"type":"string"},"viewerUrl":{"type":"string"},"glbUrl":{"type":"string","description":"The GLB WEARFITS serves for this model after autofit — i.e. the file the viewer actually renders, re-exported on their side. Distinct from `enhancedGlbUrl` (what we uploaded). Absent on legacy entries and whenever autofit did not report a GLB."}},"required":["modelId","colorId","viewerUrl"]}},"required":["model","provider","status","timeMs"]}},"finalResult":{"type":"string"}},"required":["id","type","timestamp","mode","inputs","outputs"]}},"poseTransfers":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string","enum":["pose-transfer"]},"timestamp":{"type":"string"},"status":{"type":"string","enum":["queued","processing","completed","failed"]},"inputs":{"type":"object","properties":{"sourceImage":{"type":"string"},"poseImage":{"type":"string"},"poseId":{"type":"string"}},"required":["sourceImage"]},"outputs":{"type":"array","items":{"type":"object","properties":{"model":{"type":"string","description":"The model that actually produced this output (may differ from `requestedModel` after an automatic shoe-3d fallback)."},"requestedModel":{"type":"string","description":"The model the client originally requested. Present on shoe-3d entries submitted after the fallback feature shipped."},"fallbackAttempts":{"type":"array","items":{"type":"object","properties":{"model":{"type":"string"},"chainIndex":{"type":"integer","minimum":0},"reason":{"type":"string"}},"required":["model","chainIndex","reason"]},"description":"Per-attempt audit when the fallback chain advanced (failed model + reason). Empty/undefined for jobs that completed on the primary."},"provider":{"type":"string"},"textureGenModel":{"type":"string"},"status":{"type":"string","enum":["completed","failed","processing","queued"]},"resultUrl":{"type":"string"},"renderGridUrl":{"type":"string"},"timeMs":{"type":"number"},"error":{"type":"string"},"fileSize":{"type":"number"},"meshStats":{"type":"object","properties":{"vertices":{"type":"number"},"triangles":{"type":"number"},"fileSizeBytes":{"type":"number"}},"required":["vertices","triangles","fileSizeBytes"]},"simplifiedStats":{"type":"object","properties":{"vertices":{"type":"number"},"triangles":{"type":"number"},"fileSizeBytes":{"type":"number"}},"required":["vertices","triangles","fileSizeBytes"]},"simplifiedGlbUrl":{"type":"string"},"textureGridInputUrl":{"type":"string"},"textureGridOutputUrl":{"type":"string"},"enhancedGlbUrl":{"type":"string"},"wearfitsGlbSize":{"type":"number"},"logoGridOutputUrl":{"type":"string"},"logoModelUsed":{"type":"string"},"logoRefineError":{"type":"string"},"logoRefineProcessing":{"type":"boolean"},"numViews":{"type":"number"},"textureRegenerationId":{"type":"string"},"textureRegeneratedFrom":{"type":"integer","minimum":0},"textureRegenerationRequestedAt":{"type":"string"},"textureRegenerationDispatchedAt":{"type":"string"},"textureOptions":{"type":"object","properties":{"gapFillReproject":{"type":"boolean"},"aoBake":{"type":"string","enum":["baked","off"]},"aoStrength":{"type":"number","minimum":0,"maximum":1},"source":{"type":"object","properties":{"gapFillReproject":{"type":"string","enum":["default","explicit"]},"aoBake":{"type":"string","enum":["default","explicit"]}},"required":["gapFillReproject","aoBake"]}},"description":"Effective texture options of this texture regeneration variant (absent on legacy variants = none)."},"textureOptionsKey":{"type":"string","description":"Cache key of the texture options that actually produced this output (`none` = gap fill and AO off)."},"textureOptionsFallback":{"type":"boolean","description":"True when enhancement with default-on texture options failed and was re-run without them."},"textureOptionsFallbackErrorCode":{"type":"string","description":"Error code of the failed default-options attempt (with textureOptionsFallback or textureOptionsFallbackSkipped)."},"textureOptionsFallbackSkipped":{"type":"string","enum":["deadline"],"description":"The default-options attempt failed but the fallback was skipped because stage 2 was past its deadline."},"gapFill":{"type":"object","additionalProperties":{"nullable":true},"description":"Gap-fill re-projection stats from Modal. Absent = not requested (or legacy entry)."},"aoBake":{"type":"object","additionalProperties":{"nullable":true},"description":"Ambient-occlusion bake stats from Modal (mode/strength/timings, possibly error/reason). Absent = not requested."},"arTryonError":{"type":"string"},"wearfitsTryOn":{"type":"object","properties":{"modelId":{"type":"string"},"colorId":{"type":"string"},"viewerUrl":{"type":"string"},"glbUrl":{"type":"string","description":"The GLB WEARFITS serves for this model after autofit — i.e. the file the viewer actually renders, re-exported on their side. Distinct from `enhancedGlbUrl` (what we uploaded). Absent on legacy entries and whenever autofit did not report a GLB."}},"required":["modelId","colorId","viewerUrl"]}},"required":["model","provider","status","timeMs"]}},"finalResult":{"type":"string"}},"required":["id","type","timestamp","inputs","outputs"]}},"shoe3ds":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string","enum":["shoe-3d"]},"timestamp":{"type":"string"},"status":{"type":"string","enum":["queued","processing","completed","failed"]},"error":{"type":"string"},"timings":{"type":"object","properties":{"queueMs":{"type":"number"},"totalMs":{"type":"number"}}},"inputs":{"type":"object","properties":{"images":{"type":"array","items":{"type":"string"}},"models":{"type":"array","items":{"type":"string"},"description":"The models the client originally requested (primary only). The actual model that produced each output lives in `outputs[i].model`, with the requested primary repeated as `outputs[i].requestedModel` so monitoring stays readable even after an automatic fallback."},"packshotGenModel":{"type":"string"},"packshot":{"type":"string","description":"AI-corrected packshot URL (when correctImage produced a clean studio render before generation)."},"packshotCleanup":{"$ref":"#/components/schemas/PackshotCleanup"},"packshotSingleShoe":{"$ref":"#/components/schemas/PackshotSingleShoe"},"textureOptions":{"type":"object","properties":{"gapFillReproject":{"type":"boolean"},"aoBake":{"type":"string","enum":["baked","off"]},"aoStrength":{"type":"number","minimum":0,"maximum":1},"source":{"type":"object","properties":{"gapFillReproject":{"type":"string","enum":["default","explicit"]},"aoBake":{"type":"string","enum":["default","explicit"]}},"required":["gapFillReproject","aoBake"]}},"description":"Effective texture options for this job (gapFillReproject / aoBake / aoStrength) with `source` per flag (default or explicit). Legacy entries: only the options that were ON, no source; absent = none."},"cacheImageHash":{"type":"string","description":"Geometry-cache image hash (the `<hash>` in `shoe3d:<hash>:<model>`), persisted so Clear Cache targets the exact KV key even for base64 / >4-image jobs. Absent on legacy entries."}},"required":["images","models"]},"outputs":{"type":"array","items":{"type":"object","properties":{"model":{"type":"string","description":"The model that actually produced this output (may differ from `requestedModel` after an automatic shoe-3d fallback)."},"requestedModel":{"type":"string","description":"The model the client originally requested. Present on shoe-3d entries submitted after the fallback feature shipped."},"fallbackAttempts":{"type":"array","items":{"type":"object","properties":{"model":{"type":"string"},"chainIndex":{"type":"integer","minimum":0},"reason":{"type":"string"}},"required":["model","chainIndex","reason"]},"description":"Per-attempt audit when the fallback chain advanced (failed model + reason). Empty/undefined for jobs that completed on the primary."},"provider":{"type":"string"},"textureGenModel":{"type":"string"},"status":{"type":"string","enum":["completed","failed","processing","queued"]},"resultUrl":{"type":"string"},"renderGridUrl":{"type":"string"},"timeMs":{"type":"number"},"error":{"type":"string"},"fileSize":{"type":"number"},"meshStats":{"type":"object","properties":{"vertices":{"type":"number"},"triangles":{"type":"number"},"fileSizeBytes":{"type":"number"}},"required":["vertices","triangles","fileSizeBytes"]},"simplifiedStats":{"type":"object","properties":{"vertices":{"type":"number"},"triangles":{"type":"number"},"fileSizeBytes":{"type":"number"}},"required":["vertices","triangles","fileSizeBytes"]},"simplifiedGlbUrl":{"type":"string"},"textureGridInputUrl":{"type":"string"},"textureGridOutputUrl":{"type":"string"},"enhancedGlbUrl":{"type":"string"},"wearfitsGlbSize":{"type":"number"},"logoGridOutputUrl":{"type":"string"},"logoModelUsed":{"type":"string"},"logoRefineError":{"type":"string"},"logoRefineProcessing":{"type":"boolean"},"numViews":{"type":"number"},"textureRegenerationId":{"type":"string"},"textureRegeneratedFrom":{"type":"integer","minimum":0},"textureRegenerationRequestedAt":{"type":"string"},"textureRegenerationDispatchedAt":{"type":"string"},"textureOptions":{"type":"object","properties":{"gapFillReproject":{"type":"boolean"},"aoBake":{"type":"string","enum":["baked","off"]},"aoStrength":{"type":"number","minimum":0,"maximum":1},"source":{"type":"object","properties":{"gapFillReproject":{"type":"string","enum":["default","explicit"]},"aoBake":{"type":"string","enum":["default","explicit"]}},"required":["gapFillReproject","aoBake"]}},"description":"Effective texture options of this texture regeneration variant (absent on legacy variants = none)."},"textureOptionsKey":{"type":"string","description":"Cache key of the texture options that actually produced this output (`none` = gap fill and AO off)."},"textureOptionsFallback":{"type":"boolean","description":"True when enhancement with default-on texture options failed and was re-run without them."},"textureOptionsFallbackErrorCode":{"type":"string","description":"Error code of the failed default-options attempt (with textureOptionsFallback or textureOptionsFallbackSkipped)."},"textureOptionsFallbackSkipped":{"type":"string","enum":["deadline"],"description":"The default-options attempt failed but the fallback was skipped because stage 2 was past its deadline."},"gapFill":{"type":"object","additionalProperties":{"nullable":true},"description":"Gap-fill re-projection stats from Modal. Absent = not requested (or legacy entry)."},"aoBake":{"type":"object","additionalProperties":{"nullable":true},"description":"Ambient-occlusion bake stats from Modal (mode/strength/timings, possibly error/reason). Absent = not requested."},"arTryonError":{"type":"string"},"wearfitsTryOn":{"type":"object","properties":{"modelId":{"type":"string"},"colorId":{"type":"string"},"viewerUrl":{"type":"string"},"glbUrl":{"type":"string","description":"The GLB WEARFITS serves for this model after autofit — i.e. the file the viewer actually renders, re-exported on their side. Distinct from `enhancedGlbUrl` (what we uploaded). Absent on legacy entries and whenever autofit did not report a GLB."}},"required":["modelId","colorId","viewerUrl"]}},"required":["model","provider","status","timeMs"]}},"wearfitsTryOn":{"type":"object","properties":{"modelId":{"type":"string"},"colorId":{"type":"string"},"viewerUrl":{"type":"string"},"glbUrl":{"type":"string","description":"The GLB WEARFITS serves for this model after autofit — the file the viewer actually renders. Distinct from the per-output `enhancedGlbUrl` (what we uploaded). Absent on legacy entries."}},"required":["modelId","colorId","viewerUrl"]}},"required":["id","type","timestamp","inputs","outputs"]}},"lastUpdated":{"type":"string"}},"required":["digitalTwins","tryons","poseTransfers","shoe3ds","lastUpdated"]}}}},"403":{"description":"Access denied - IP not allowed"}}}},"/monitoring/api/tryons/{id}":{"delete":{"tags":["Monitoring"],"security":[],"summary":"Delete try-on monitoring entry","parameters":[{"schema":{"type":"string","description":"Monitoring entry ID"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Try-on entry deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[true]},"deletedFiles":{"type":"number"}},"required":["success","deletedFiles"]}}}},"404":{"description":"Entry not found"}}}},"/monitoring/api/tryons/{id}/outputs/{index}":{"delete":{"tags":["Monitoring"],"security":[],"summary":"Delete try-on output","parameters":[{"schema":{"type":"string","description":"Monitoring entry ID"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","description":"Output index"},"required":true,"name":"index","in":"path"}],"responses":{"200":{"description":"Try-on output deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[true]},"deletedFiles":{"type":"number"}},"required":["success","deletedFiles"]}}}},"404":{"description":"Entry or output not found"}}}},"/monitoring/api/digital-twins/{id}":{"delete":{"tags":["Monitoring"],"security":[],"summary":"Delete digital twin monitoring entry","parameters":[{"schema":{"type":"string","description":"Monitoring entry ID"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Digital twin entry deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[true]},"deletedFiles":{"type":"number"}},"required":["success","deletedFiles"]}}}},"404":{"description":"Entry not found"}}}},"/monitoring/api/digital-twins/{id}/outputs/{index}":{"delete":{"tags":["Monitoring"],"security":[],"summary":"Delete digital twin output","parameters":[{"schema":{"type":"string","description":"Monitoring entry ID"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","description":"Output index"},"required":true,"name":"index","in":"path"}],"responses":{"200":{"description":"Digital twin output deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[true]},"deletedFiles":{"type":"number"}},"required":["success","deletedFiles"]}}}},"404":{"description":"Entry or output not found"}}}},"/monitoring/api/pose-transfers/{id}":{"delete":{"tags":["Monitoring"],"security":[],"summary":"Delete pose transfer monitoring entry","parameters":[{"schema":{"type":"string","description":"Monitoring entry ID"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Pose transfer entry deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[true]},"deletedFiles":{"type":"number"}},"required":["success","deletedFiles"]}}}},"404":{"description":"Entry not found"}}}},"/monitoring/api/pose-transfers/{id}/outputs/{index}":{"delete":{"tags":["Monitoring"],"security":[],"summary":"Delete pose transfer output","parameters":[{"schema":{"type":"string","description":"Monitoring entry ID"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","description":"Output index"},"required":true,"name":"index","in":"path"}],"responses":{"200":{"description":"Pose transfer output deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[true]},"deletedFiles":{"type":"number"}},"required":["success","deletedFiles"]}}}},"404":{"description":"Entry or output not found"}}}},"/monitoring/api/shoe3ds/{id}":{"delete":{"tags":["Monitoring"],"security":[],"summary":"Delete shoe 3D monitoring entry","parameters":[{"schema":{"type":"string","description":"Monitoring entry ID"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Shoe 3D entry deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[true]},"deletedFiles":{"type":"number"}},"required":["success","deletedFiles"]}}}},"404":{"description":"Entry not found"}}}},"/monitoring/api/shoe3ds/{id}/outputs/{index}":{"delete":{"tags":["Monitoring"],"security":[],"summary":"Delete shoe 3D output","parameters":[{"schema":{"type":"string","description":"Monitoring entry ID"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","description":"Output index"},"required":true,"name":"index","in":"path"}],"responses":{"200":{"description":"Shoe 3D output deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[true]},"deletedFiles":{"type":"number"}},"required":["success","deletedFiles"]}}}},"404":{"description":"Entry or output not found"}}}},"/monitoring/api/shoe3ds/{id}/clear-cache":{"post":{"tags":["Monitoring"],"security":[],"summary":"Clear KV cache for a shoe 3D entry","description":"Deletes the global geometry cache entry and all per-API-key WEARFITS link cache entries for the input images of this job, so the next request regenerates from scratch.","parameters":[{"schema":{"type":"string","description":"Monitoring entry ID"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Cache cleared","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"deleted":{"type":"array","items":{"type":"string"}}},"required":["success","deleted"]}}}},"404":{"description":"Entry not found"}}}},"/monitoring/api/shoe3ds/{id}/outputs/{index}/ar-tryon":{"post":{"tags":["Monitoring"],"security":[],"summary":"Generate AR try-on from shoe 3D output","description":"Uploads a completed shoe 3D GLB to WEARFITS for AR try-on. Returns immediately with processing status.","parameters":[{"schema":{"type":"string","description":"Monitoring entry ID"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","description":"Output index"},"required":true,"name":"index","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"rightShoe":{"type":"boolean","default":false,"description":"Whether this is a right shoe model (default: false/left)"}}}}}},"responses":{"200":{"description":"AR try-on completed successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[true]},"modelId":{"type":"string","description":"WEARFITS model ID"},"colorId":{"type":"string","description":"WEARFITS color variant ID"},"viewerUrl":{"type":"string","description":"URL to view shoe in AR try-on"}},"required":["success","modelId","colorId","viewerUrl"]}}}},"202":{"description":"AR try-on upload accepted and processing","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[true]},"status":{"type":"string","enum":["processing"],"description":"Processing status - upload runs asynchronously"},"message":{"type":"string","description":"Status message"}},"required":["success","status","message"]}}}},"400":{"description":"No GLB available or invalid output"},"404":{"description":"Entry or output not found"}}}},"/monitoring/api/shoe3ds/{id}/outputs/{index}/logo-refine":{"post":{"tags":["Monitoring"],"security":[],"summary":"Run logo refinement on shoe 3D texture","description":"Runs standalone logo refinement (second AI pass) on a previously enhanced texture grid. Synchronous — waits for Modal to complete (~90-180s).","parameters":[{"schema":{"type":"string","description":"Monitoring entry ID"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","description":"Output index"},"required":true,"name":"index","in":"path"}],"responses":{"200":{"description":"Logo refinement completed","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[true]},"logoGridOutputUrl":{"type":"string","description":"URL to logo-refined grid image"},"logoModelUsed":{"type":"string","description":"AI model used for logo refinement"}},"required":["success"]}}}},"400":{"description":"No texture grid available or missing data"},"404":{"description":"Entry or output not found"},"500":{"description":"Logo refinement failed"}}}},"/monitoring/api/shoe-texture-regeneration-models":{"get":{"tags":["Monitoring"],"security":[],"summary":"Available shoe texture regeneration models","responses":{"200":{"description":"Allowed models","content":{"application/json":{"schema":{"type":"object","properties":{"models":{"type":"array","items":{"type":"string"}}},"required":["models"]}}}}}}},"/monitoring/api/shoe3ds/{id}/outputs/{index}/regenerate-texture":{"post":{"tags":["Monitoring"],"security":[],"summary":"Regenerate shoe 3D texture with a selected model","description":"Queues a new texture variant from the original raw GLB and stored reference images. The source output is preserved. No WEARFITS upload is performed.","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"index","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"model":{"type":"string","enum":["google/gemini-3.1-flash-image","openai/gpt-image-2.5-sunburst","meta/muse-image"]},"gapFillReproject":{"type":"boolean","description":"Gap-fill re-projection for this variant (default true unless the SHOE3D_TEXTURE_DEFAULTS kill switch is off; false opts out)."},"aoBake":{"type":"string","enum":["baked","off"],"description":"Bake ambient occlusion into the base color for this variant (default \"baked\" unless the kill switch is off; \"off\" opts out)."},"aoStrength":{"type":"number","minimum":0,"maximum":1,"description":"AO strength 0..1 (Modal default 0.7 when omitted). Ignored when aoBake is off."}},"required":["model"]}}}},"responses":{"202":{"description":"Texture regeneration queued","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[true]},"outputIndex":{"type":"number","description":"Index of the new output in the outputs array"},"status":{"type":"string","enum":["processing"],"description":"Processing status - regeneration runs asynchronously"}},"required":["success","outputIndex","status"]}}}},"400":{"description":"Invalid model, index or missing source data"},"404":{"description":"Entry or output not found"},"503":{"description":"Could not persist or enqueue work"}}}},"/monitoring/api/shoe3ds/{id}/repair-autofit":{"post":{"tags":["Monitoring"],"security":[],"summary":"Repair a failed shoe autofit","description":"Operator-only retry of WEARFITS upload and autofit for an already generated shoe GLB. Optional sourceOutputIndex selects a completed texture regeneration variant owned by this job.","parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"sourceOutputIndex":{"type":"integer","minimum":0},"resolution":{"type":"string","enum":["confirmed-no-upload","resume-uploaded-model"]},"uploadedModel":{"type":"object","properties":{"modelId":{"type":"string"},"colorId":{"type":"string"},"viewerUrl":{"type":"string","format":"uri"}},"required":["modelId","colorId","viewerUrl"]}}}}}},"responses":{"200":{"description":"Repair already active or completed"},"202":{"description":"Repair queued"},"400":{"description":"Job is not eligible for autofit repair"},"404":{"description":"Shoe 3D job not found"},"409":{"description":"Previous upload outcome requires inspection"},"503":{"description":"Could not queue repair"}}},"get":{"tags":["Monitoring"],"security":[],"summary":"Get shoe autofit repair status","parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Current repair status and resulting viewer URL"},"404":{"description":"Shoe 3D job not found"}}}},"/monitoring/api/regenerate-models":{"get":{"tags":["Monitoring"],"security":[],"summary":"Get available regeneration models","description":"Returns the list of models available for regenerating try-on outputs","responses":{"200":{"description":"Available models","content":{"application/json":{"schema":{"type":"object","properties":{"openrouter":{"type":"array","items":{"type":"string"}},"fal":{"type":"array","items":{"type":"string"}},"openai":{"type":"array","items":{"type":"string"}},"xai":{"type":"array","items":{"type":"string"}}},"required":["openrouter","fal","openai","xai"]}}}}}}},"/monitoring/api/available-poses":{"get":{"tags":["Monitoring"],"security":[],"summary":"Get available pose references (experimental)","description":"Returns the list of pose images/templates available for guided regeneration","responses":{"200":{"description":"Available poses","content":{"application/json":{"schema":{"type":"object","properties":{"poses":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"imageUrl":{"type":"string"}},"required":["id","name"]}}},"required":["poses"]}}}}}}},"/monitoring/api/tryons/{id}/regenerate":{"post":{"tags":["Monitoring"],"security":[],"summary":"Regenerate try-on with different model","description":"Generates a new output for an existing try-on using a different model. Returns immediately with 202 Accepted while processing continues in background.","parameters":[{"schema":{"type":"string","description":"Monitoring entry ID"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"model":{"type":"string","description":"Model to use for regeneration","example":"google/gemini-3.1-flash-image"},"prompt":{"type":"string","description":"Optional custom prompt for regeneration"},"poseImage":{"type":"string","description":"Optional pose reference image URL (OpenPose skeleton) to guide generation pose","example":"https://example.com/pose-skeleton.png"}},"required":["model"]}}}},"responses":{"202":{"description":"Regeneration accepted and processing in background","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[true]},"outputIndex":{"type":"number","description":"Index of the new output in the outputs array"},"status":{"type":"string","enum":["processing"],"description":"Processing status - regeneration runs asynchronously"}},"required":["success","outputIndex","status"]}}}},"400":{"description":"Invalid model or missing required data"},"404":{"description":"Entry not found"}}}},"/monitoring/api/saas/users":{"get":{"tags":["Monitoring"],"security":[],"summary":"Get SaaS users","description":"Returns list of users with API key counts and package info from the database","responses":{"200":{"description":"SaaS users data","content":{"application/json":{"schema":{"type":"object","properties":{"users":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string","nullable":true},"name":{"type":"string","nullable":true},"package":{"type":"string"},"createdAt":{"type":"string"},"apiKeysCount":{"type":"number"}},"required":["id","email","name","package","createdAt","apiKeysCount"]}},"stats":{"type":"object","properties":{"total":{"type":"number"},"byPackage":{"type":"object","additionalProperties":{"type":"number"}}},"required":["total","byPackage"]},"error":{"type":"string"}},"required":["users","stats"]}}}},"403":{"description":"Access denied - IP not allowed"}}}},"/monitoring":{"get":{"tags":["Monitoring"],"security":[],"summary":"Monitoring dashboard","description":"HTML dashboard for viewing generation results","responses":{"200":{"description":"Dashboard HTML page","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/api/v1/files/upload":{"post":{"tags":["Files"],"security":[{"apiKey":[]}],"summary":"Upload a temporary file","description":"Upload a large file (like a 30MB GLB model) via multipart/form-data. This is highly recommended over sending 40MB base64 JSON payloads which exhaust Cloudflare Worker memory. Returns a robust temporary URL to pass as `glbInput` into pipelines. Files are isolated and expire automatically after a short period.","requestBody":{"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"nullable":true,"description":"The binary file payload to upload","format":"binary"}}}}}},"responses":{"201":{"description":"File uploaded successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[true]},"url":{"type":"string","format":"uri","description":"URL to the uploaded temporary file"},"filename":{"type":"string","description":"Original filename"},"size":{"type":"number","description":"File size in bytes"}},"required":["success","url","filename","size"]}}}},"400":{"description":"Bad Request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/tryon/clothing":{"post":{"tags":["Try-On"],"summary":"Submit clothing try-on request","description":"Generate a virtual try-on image without creating a digital twin. Use a raw person photo or load an existing twin image.\n\n## Person Input (choose ONE)\n\n**Option 1: `digitalTwinId` (Recommended)**\nUse a pre-generated digital twin to reuse the same face and body reference image across try-ons. Generated results can still vary.\nFirst create a twin via `POST /api/v1/digital-twin`, then use the returned ID here.\n\n```json\n{ \"digitalTwinId\": \"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\", \"topGarment\": \"https://example.com/shirt.jpg\" }\n```\n\n**Option 2: `personImages` (Direct mode — no digital twin)**\nSend a person photo directly to the try-on model. **Warning:** Face may vary between requests.\n\n```json\n{ \"personImages\": [\"https://example.com/person.jpg\"], \"topGarment\": \"https://example.com/shirt.jpg\" }\n```\n\n**Option 2b: `productImage` (Direct 2-image, no grid)**\nSingle packshot — same prompt path as `virtual-fitting` `productImage`. Pair with either\n`personImages` (no twin, raw photo) or `digitalTwinId` (cached twin loaded as the person image).\n\n```json\n{\n  \"personImages\": [\"https://example.com/model.jpg\"],\n  \"productImage\": \"https://example.com/jacket.jpg\",\n  \"productCategory\": \"top\"\n}\n```\n\n```json\n{\n  \"digitalTwinId\": \"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\",\n  \"productImage\": \"https://example.com/jacket.jpg\",\n  \"productCategory\": \"top\"\n}\n```\n\n> **Important:** You cannot use both `digitalTwinId` and `personImages` in the same request.\n\n## Garment Types\n\n- `productImage`: Single product (2-image path; cannot mix with slot garments below)\n- `topGarment`: Shirts, blouses, jackets\n- `bottomGarment`: Pants, skirts, shorts\n- `fullBodyGarment`: Dresses, jumpsuits, rompers\n- `shoes`: Optional footwear to add to any outfit\n\n## Garment Image Formats\n\n- Single URL: `\"https://example.com/garment.jpg\"`\n- Array with reference: `[\"packshot.jpg\", \"on-model.jpg\"]` - helps AI understand fit and drape\n\n## Valid Garment Combinations\n\n- Top only, Bottom only, or Top + Bottom\n- Full-body garment (dress/jumpsuit)\n- Any combination above + optional Shoes\n- A single productImage with productCategory shoe for standalone shoe image try-on","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClothingTryOnRequest"}}}},"responses":{"201":{"description":"Job created successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TryOnResponse"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/tryon/shoes":{"post":{"tags":["Try-On"],"summary":"Submit shoe try-on request","description":"Generate a shoe try-on image without creating a digital twin. Supply personImages or an existing digitalTwinId (not both), plus shoeImages as objects with url and optional angle. This endpoint does not accept productImage; use /api/v1/tryon/clothing for that shortcut. Returns a job ID for tracking. This is image generation, not a live AR viewer.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShoeTryOnRequest"}}}},"responses":{"201":{"description":"Job created successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TryOnResponse"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/generate":{"post":{"tags":["Generate"],"summary":"Generate or modify images","description":"Submit a request to generate a new image or modify existing images using AI. Returns a job ID for tracking.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateRequest"}}}},"responses":{"201":{"description":"Job created successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateResponse"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/digital-twin":{"post":{"tags":["Digital Twin"],"summary":"Generate a reusable digital twin avatar","description":"Create a digital twin avatar for reuse in virtual try-ons. Every mode requires faceImage plus exactly one body source: bodyPhotoUrl, silhouetteImage, bodyMeasurements or clothingSize.\n\n## Photo Mode (Recommended for most apps)\n\nUse this when your users upload regular photos from their phone/camera.\n\n**Request:**\n```json\n{\n  \"faceImage\": \"https://example.com/face-selfie.jpg\",\n  \"bodyPhotoUrl\": \"https://example.com/full-body-photo.jpg\"\n}\n```\n\n**What happens automatically:**\n1. SAM-3D extracts a body mesh from the full-body photo\n2. Pose transfer applies the requested pose\n3. Digital twin generation uses the face and body references\n\n**Preserve original pose:**\nBy default, a standard pose is applied. To keep the original pose from the photo, set `options.preservePose: true`:\n```json\n{\n  \"faceImage\": \"https://example.com/face.jpg\",\n  \"bodyPhotoUrl\": \"https://example.com/body.jpg\",\n  \"options\": { \"preservePose\": true }\n}\n```\n\n**Photo requirements:**\n- `faceImage`: Clear face photo (selfie works great)\n- `bodyPhotoUrl`: Full-body photo showing head to feet, person standing\n\n**Choose a pose for try-on:**\nSpecify `poseId` to control the avatar pose:\n- `default`: Resolved from deployment configuration and gender\n- `girl_pose`: Female standing pose\n- `man_pose`: Male standing pose\n- `shoe_girl_pose`: Pose with visible feet (for shoe try-ons)\n- `standing_arms_down`: Arms at sides\n\n**Important:** Each pose creates a **separate cached twin**. If you need the same person in multiple poses, create a twin for each pose. The `digitalTwinId` includes the pose - you cannot change it later.\n\n---\n\n## Direct Mode (Advanced - for custom 3D pipelines)\n\nUse this only if you have your own 3D body scanning/rendering system.\n\n**Request:**\n```json\n{\n  \"faceImage\": \"https://example.com/face.jpg\",\n  \"silhouetteImage\": \"https://example.com/depth-render.png\"\n}\n```\n\nThe `silhouetteImage` must be a pre-rendered depth visualization from a 3D mesh, NOT a regular photo.\n\n---\n\n## Response\n\nAn uncached request returns a job ID and status URL. Poll the completed job for its `digitalTwinId` (64-character hex string). A twin-cache hit returns the ID immediately.\n\n**Use the ID for subsequent try-ons:**\nSend the saved ID as `digitalTwinId` with garment inputs to either POST /api/v1/virtual-fitting or POST /api/v1/tryon/clothing.\n\n## Measurements and clothing size\n\nInstead of a body photo, pair `faceImage` with `bodyMeasurements` (at least two supported measurements) or `clothingSize` (size and height). These modes can store a body profile for estimated size fitting. Photo and direct-silhouette twins are not eligible for that profile-based feature. Check POST /api/v1/size-fitting for availability and chart requirements.\n\n**Benefits:**\n- Reuses the generated face and body image across try-ons\n- Cached for 30 days\n- No need to re-upload photos for each try-on\n- Skips twin generation on subsequent try-ons; try-on image generation still takes time\n\n## Processing Time\nProcessing time varies with input mode, provider load and cache availability. The response's estimatedProcessingTime is an estimate, not a deadline. Poll the job until a terminal status.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DigitalTwinRequest"}}}},"responses":{"201":{"description":"Job created successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DigitalTwinResponse"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/digital-twin/{id}":{"get":{"tags":["Digital Twin"],"summary":"Check if a digital twin ID is valid","description":"Check if a digital twin ID exists and is available for use in try-on or virtual-fitting requests.\n\nUse this endpoint to verify a cached digital twin is still valid before submitting a job.\nDigital twins expire after 30 days from creation.","parameters":[{"schema":{"type":"string","minLength":1,"description":"The digital twin ID to check","example":"c3721f86f03c1d7035f0728cad2ca97d027781dc7e1722cf92901e10f56007c0"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Digital twin status","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DigitalTwinStatusResponse"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/generate-3d":{"post":{"tags":["Generate 3D"],"summary":"Generate a 3D model from an image","description":"Submit an image to generate a 3D model. Supports multiple models. Returns a job ID for tracking.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Generate3DRequest"}}}},"responses":{"201":{"description":"Job created successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Generate3DResponse"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/shoe-3d":{"post":{"tags":["Shoe 3D"],"summary":"Generate 3D models of shoes from photos","description":"Generate 3D models of shoes from one or more 2D photos using AI.\n\n## Overview\n\nThis endpoint converts shoe photographs into 3D GLB models using **hunyuan-3d** as the generative backbone, followed by an AI texture-enhancement and optional WEARFITS AR upload pipeline.\n\n**Automatic model fallback:** if the primary fal.ai model fails in a **model-scoped** way (submit error, gateway `RESULT_UNAVAILABLE`, or a FAILED terminal status), the pipeline automatically falls back to **tripo3d/h3.1** and re-submits. Up to 4 attempts are made in total, alternating primary → fallback → primary → fallback. Clients do not need to retry on `job.failed` caused by a single-model outage — the processor has already exhausted the chain internally. Per-attempt history is recorded in job monitoring under `modelStates[i].attempts`.\n\n**Exception — account-level provider failures:** when fal.ai reports an account-level problem (locked account, exhausted balance, insufficient funds, invalid key) the remaining chain slots are **skipped** and the job fails right away. All slots run on the same fal.ai account, so further attempts cannot succeed and would only delay the failure. The failure still carries `ALL_FALLBACKS_FAILED`, with fewer entries in `fallbackAttempts` than the full chain and a neutral message — provider account/billing prose is redacted from client payloads, while `error.code` and `error.retryable` are never rewritten.\n\n## Basic Usage\n\n```json\n{\n  \"images\": [\"https://example.com/shoe.jpg\"]\n}\n```\n\n## Image Recommendations\n\n- **Input:** Clear photo of the shoe with good lighting\n- **Angle:** Show front and side of the shoe for best results\n- **Background:** Plain/solid background works best\n- **Resolution:** Minimum 512x512, recommended 1024x1024\n\n## Image Validation & AI Correction (Default: In-Queue)\n\nBy default, image validation and AI correction run **asynchronously in the queue** after the job is created. This allows for faster API response times.\n\n**Photo requirements checked (during queue processing):**\n- Good, constant lighting (no harsh shadows)\n- Clean, neutral background\n- Only ONE shoe visible\n- Shoe fills the entire frame (no cropping)\n- No reflective lights, people, hands, or watermarks\n\n**Left/Right shoe detection:** The validator detects if the shoe is left or right based on toe direction.\n\n**Multiple images:** When providing multiple photos, the best image is automatically selected for 3D generation.\n\n**Validation strictness when `correctImage: true` (default):** Only `NOT_A_SHOE` causes a hard failure. Quality issues (cropped frame, back/sole-only view, poor lighting, etc.) are tolerated — the AI corrector is given the chance to generate a proper packshot from the best available image, and if correction fails the original image is passed directly to 3D generation. Use `correctImage: false` for strict quality gating.\n\n## AI Image Correction (Default: Enabled)\n\nUser-uploaded photos are automatically transformed into professional packshot images using Gemini 3.1 Flash before 3D generation. This significantly improves 3D model quality by:\n\n- Generating a clean white background (no shadows or textures)\n- Positioning the shoe at the optimal 3/4 front-side angle\n- Applying studio-quality lighting\n- Preserving exact colors, textures, and brand details\n\nThe corrected image is then validated and used as input for 3D generation.\n\nTo disable correction and use original images directly:\n```json\n{\n  \"images\": [\"https://example.com/shoe.jpg\"],\n  \"options\": {\n    \"correctImage\": false\n  }\n}\n```\n\n## Synchronous Validation Mode\n\nFor use cases where immediate feedback on validation is needed, set `validateInQueue: false`:\n\n```json\n{\n  \"images\": [\"https://example.com/shoe.jpg\"],\n  \"options\": {\n    \"validateInQueue\": false\n  }\n}\n```\n\nWith `validateInQueue: false`:\n- Validation and correction run synchronously during the API request\n- Returns 400 error immediately if validation fails\n- Slower response time due to synchronous processing\n\nWith `validateInQueue: true` (default):\n- The request returns immediately with job ID (no pre-validation)\n- Validation runs asynchronously in the queue\n- Hard failure (`SHOE_IMAGE_VALIDATION_FAILED`) only when no image is a shoe (`NOT_A_SHOE`); quality issues (cropped, poor angle, etc.) are tolerated when `correctImage: true`\n\n## Custom GLB Bypassing (Memory-Safe Uploads)\n\nIf you already possess a custom 3D model, `glbInput` skips AI geometry generation. Pre-simplification targets 100k triangles on a best-effort basis; if no usable simplified model URL is returned, processing continues with the original, so this is not an enforced maximum. Texture enhancement and WEARFITS publication follow their respective options; texture enhancement can still invoke AI.\n\nBecause 30MB+ GLB models converted to Base64 JSON strings can cause Cloudflare Worker RAM exhaustion (OOM), we provide a dedicated streaming upload endpoint.\n\n**Step 1: Upload the GLB Model**\n```bash\ncurl -X POST 'https://api.wearfits.com/api/v1/files/upload' \\\n  -H 'X-API-Key: your_api_key' \\\n  -F \"file=@my_model.glb\"\n```\n*(Returns a temporary `url`)*\n\n**Step 2: Run the Pipeline**\n```json\n{\n  \"images\": [\"https://example.com/shoe.jpg\"],\n  \"glbInput\": \"https://api.wearfits.com/files/signed?key=temp%2Fuser-uploads%2F...\",\n  \"options\": {\n    \"enhanceTexture\": true,\n    \"uploadToWearFits\": true\n  }\n}\n```\n\n## WEARFITS AR Integration\n\nEnable `uploadToWearFits` to automatically upload the result to WEARFITS for AR try-on:\n\n```json\n{\n  \"images\": [\"https://example.com/shoe.jpg\"],\n  \"options\": {\n    \"uploadToWearFits\": true,\n    \"sessionId\": \"user-session-id\"\n  }\n}\n```\n\n## Who the uploaded model belongs to\n\nWith `uploadToWearFits: true` the generated model is filed in a WEARFITS library. Two mechanisms decide whose, and **they are mutually exclusive** — WEARFITS rejects an upload that claims two owners:\n\n1. **The account behind your credential (automatic).** Any credential that resolves to a WEARFITS account — a dashboard API key, or a service key carrying a verified account token — files the model in that account's library. Nothing to send: it is resolved when the job is submitted. This is not limited to one kind of token; if the request authenticated as an account, that account owns the model.\n2. **`options.sessionId` (explicit).** A frontend app that already holds a WEARFITS user session can name it, and the model is linked to that session instead.\n\n**`sessionId` wins.** When you send one, it is the only ownership claim sent and the account behind the credential is not used — naming a session is an explicit choice and this API does not override it.\n\nSend neither and the model is filed under the account that owns this deployment's WEARFITS token, which is a shared library, not yours.\n\n## Processing Time\n\nTime varies with provider queues, cache availability, image processing, texture enhancement and optional publication. Poll the job until a terminal status; estimatedProcessingTime is not a deadline.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Shoe3DRequest"}}}},"responses":{"201":{"description":"Job created successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Shoe3DResponse"}}}},"400":{"description":"Invalid request or shoe image validation failed","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/Shoe3DValidationError"},{"$ref":"#/components/schemas/ErrorResponse"}]}}}}}}},"/api/v1/tools/mirror-logos":{"post":{"tags":["Tools"],"summary":"Generate the mirrored (L/P) shoe texture for AR pairs","description":"Generate the second (left-foot / L-P) albedo texture for an AR shoe pair.\n\n## Overview\n\nThe AR fitting room mirrors one shoe mesh to render the other foot, which makes every\nlogo, wordmark, and emblem read backwards on the mirrored foot. WEARFITS supports two\ntextures per shoe (an L/P slot); this endpoint produces that second texture: identical\nto the original everywhere except the brand marks, which are repaired to read correctly\non the mirrored mesh.\n\n## Async job\n\nThis endpoint returns immediately with a `jobId`. Poll `GET /api/v1/jobs/{jobId}` until\n`status` is `completed` or `failed`. On completion the job carries:\n\n```json\n{\n  \"status\": \"completed\",\n  \"output\": {\n    \"textureUrl\": \"https://.../signed?key=...\",\n    \"report\": { \"clusters\": [], \"unrepaired\": [], \"version\": \"...\" }\n  }\n}\n```\n\nMarks the pipeline could not repair are left mirrored and listed in `report` — that is the\nsignal to re-run. Typical processing time is ~5 minutes.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MirrorLogosRequest"}}}},"responses":{"202":{"description":"Job accepted and queued for processing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MirrorLogosResponse"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"Service not configured (MIRROR_LOGOS_TOKEN unset on the deployment)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/product-discovery":{"post":{"tags":["Product Discovery"],"summary":"Discover a matching product on a shop URL and return packshot images","description":"Given a shop domain, category page, or product URL, this endpoint crawls the site with Cloudflare Browser Rendering and returns 3–4 packshot images of the first matching product in the requested category. The returned images can be fed into `POST /api/v1/shoe-3d` to generate a 3D model (see \"Chaining into shoe-3d\" below).\n\n## Categories\n\n- `shoes` (default) — low-profile shoes: sneakers, loafers, moccasins, sport/running. Excludes high heels and boots.\n- `bags` — handheld handbags/purses (\"torebka do ręki\"). Excludes backpacks, suitcases, wallets.\n\n## Typical request\n\n```json\n{ \"url\": \"https://example-shop.com\", \"category\": \"shoes\" }\n```\n\n## How the crawl works\n\n1. The endpoint visits `url` and analyses the page (og:type, JSON-LD Product count, product-URL hints, add-to-cart signals, large image galleries) to classify it as `product`, `uncertain`, or `catalog`.\n2. If the page is `product` or `uncertain` and has ≥3 candidate packshots, it is sent to a vision LLM (Gemini) for category/same-product validation **before any navigation**.\n3. Only if the LLM rejects (or if the page is clearly a `catalog`) does the crawler follow up to `options.maxNavDepth` hops of category / product links, re-running the same validation at each step.\n4. The first page whose images pass LLM validation is returned. Passing the URL of a real PDP will therefore normally return results without any navigation.\n\n## Response\n\n- `images` — 3–4 URLs, best-first, ready to feed into the 3D pipeline.\n- `pageUrl` — the product page the images came from.\n- `classification` — the LLM's judgement about whether the images really are of the requested category and depict the same product.\n- `navigationPath` — pages visited during the crawl, useful for debugging. `kind` is one of `home` / `category` / `product` / `unknown` (the last reflects pages analysed as \"uncertain\").\n\n## Chaining into shoe-3d\n\n- `category: \"shoes\"` — feed `images` directly into `POST /api/v1/shoe-3d`; its default `options.productType: \"shoe\"` is correct.\n- `category: \"bags\"` — feed `images` **and** set `options.productType: \"other\"` (or `\"auto\"`) on the shoe-3d request. Without this the shoe-specific prompts and orientation will be applied to a handbag.\n\n## Errors\n\n- `400 VALIDATION_ERROR` — request body failed Zod validation (e.g. missing or malformed `url`, unknown `category`). Emitted by the global error handler with a `details` array.\n- `401 HTTP_ERROR` — missing API key.\n- `403 HTTP_ERROR` — invalid / deactivated / expired API key.\n- `404 NO_PRODUCT_FOUND` — no matching product could be found within the crawl/LLM budget. Response includes `navigationPath` and, when available, the LLM's rejection reason.\n- `504 DISCOVERY_TIMEOUT` — exceeded `options.timeoutMs`.\n- `500 DISCOVERY_FAILED` — browser or LLM provider error.\n\n## Notes\n\n- Implementation is synchronous — the response arrives when crawling + LLM validation complete (typically 10–60 s). Structured so a future migration to `JOB_QUEUE` + polling is a wrapper change only.\n- Browser Rendering quota counts against the Cloudflare plan of the worker.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProductDiscoveryRequest"}}}},"responses":{"200":{"description":"Matching product found (synchronous mode — default).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProductDiscoveryResponse"}}}},"201":{"description":"Async job created (when `options.async: true`). Poll `statusUrl` until completion; result lands under `productDiscoveryResult` on the job record.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProductDiscoveryAsyncResponse"}}}},"400":{"description":"Request validation failed (e.g. missing/invalid `url` or `category`). Emitted by the global error handler with code `VALIDATION_ERROR` and a `details` array.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing API key (X-API-Key header).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Invalid, deactivated, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No matching product was found on the given URL.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProductDiscoveryErrorResponse"}}}},"500":{"description":"Discovery failed (browser or LLM provider error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProductDiscoveryErrorResponse"}}}},"504":{"description":"Discovery exceeded the timeout budget.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProductDiscoveryErrorResponse"}}}}}}},"/api/v1/virtual-fitting":{"post":{"tags":["Virtual Fitting"],"summary":"Twin-backed virtual fitting","description":"Creates or reuses a digital twin and applies garments in one request. For raw-photo try-on without generating a twin, use POST /api/v1/tryon/clothing instead.\n\n## Quick Start\n\n```json\n{\n  \"faceImage\": \"https://example.com/face.jpg\",\n  \"silhouetteImage\": \"https://example.com/silhouette.png\",\n  \"topGarment\": \"https://example.com/shirt.jpg\",\n  \"bottomGarment\": \"https://example.com/pants.jpg\"\n}\n```\n\n## Response\n\nA queued submission returns a job ID and status URL. Poll the completed job for its `digitalTwinId` and results, then save the twin ID for future try-ons. A result-cache hit can return completed results and `digitalTwinId` immediately.\n\n## Reusing the Digital Twin\n\nFor subsequent try-ons, pass only the `digitalTwinId` (faster, skips twin generation):\n```json\n{\n  \"digitalTwinId\": \"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\",\n  \"topGarment\": \"https://example.com/different-shirt.jpg\"\n}\n```\n\n---\n\n## Input Modes\n\n**Mode 1: Direct (pre-rendered silhouette)**\nProvide `faceImage` + `silhouetteImage` directly.\n\n**Mode 2: Photo (Auto body extraction)**\nProvide `faceImage` + `photoUrl`; `poseId` is optional. The worker requires both images to generate a new photo-mode twin. A photoUrl-only request can pass request validation but fail asynchronously with MISSING_INPUTS.\n- Requires full-body photo showing head to feet\n\n**Mode 3: Twin ID (Fastest)**\nProvide only `digitalTwinId` from a previous request - skips twin generation entirely.\n\n**Mode 4: Measurements**\nProvide `faceImage` + `bodyMeasurements` - generates body from sizing data.\n\n**Mode 5: Clothing size**\nProvide `faceImage` + `clothingSize` (size and height).\n\n---\n\n## Available Poses (photo, measurements and clothing-size modes)\n\n- `default`: System default pose\n- `girl_pose`: Female standing pose\n- `man_pose`: Male standing pose\n- `shoe_girl_pose`: Visible feet (for shoe try-ons)\n- `standing_arms_down`: Arms at sides\n\n---\n\n## Caching\n\n- **Digital twins:** Cached 30 days. Same face + silhouette = same `digitalTwinId`\n- **Results:** Cached 7 days. Matching cache inputs can reuse a completed result without new image generation.\n- Use `options.skipCache: true` to bypass the twin cache and `options.skipResultCache: true` to bypass the final try-on cache.\n\n## Garment Formats\n\n- Single URL: `\"https://example.com/garment.jpg\"`\n- With reference: `[\"packshot.jpg\", \"on-model.jpg\"]` - helps AI understand fit\n- Single product shortcut: `productImage` plus optional `productCategory` (`auto`, `top`, `bottom`, `full-body`, `shoe`). When exactly one product image is provided, the try-on model receives the person/digital twin and that product image directly, without a garment grid.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VirtualFittingRequest"}}}},"responses":{"201":{"description":"Job created successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VirtualFittingResponse"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/storefront/tokens":{"post":{"tags":["Storefront"],"summary":"Mint a storefront or account-attribution token","description":"Issue a short-lived HMAC token that maps a merchant to their SaaS account without putting an exhaustible dashboard API key in a shopper's browser.\n\n**Browser widget:** mint `audience: \"storefront\"` server-side and put the `wfs1.` token in `data-storefront-token` / `X-API-Key`. The token can only `POST /api/v1/virtual-fitting` and `GET /api/v1/jobs/{id}`. Reminting for the same account still polls in-flight jobs.\n\n**Hosted fitting room / service-key proxy:** mint `audience: \"account\"` and send it as `X-Wearfits-Account` next to the deployment service key. AI quota is counted; the key-usage counter is not (there is no raw merchant key).\n\n**Auth (one of):**\n- A database-backed merchant key (`X-API-Key`) — mints for that account.\n- A service key plus a valid `X-Wearfits-Account` token — remints for that account.\n- Handshake bootstrap: body `handshake` + `userId` (the shared `DEV_INTEGRATION_HANDSHAKE` secret). No CORS-open widget path; this is for dash / server callers.\n- `audience: \"usage\"` (`wfu1.`, analytics-only `X-Wearfits-Usage-Account`): ONLY handshake + `userId` with the separate `USAGE_TOKEN_HANDSHAKE` secret; `DEV_INTEGRATION_HANDSHAKE` cannot mint it and it cannot mint anything else. Unset = 401.\n\nReturns 503 when the handshake secret is unset on this deployment.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StorefrontTokenRequest"}}}},"responses":{"200":{"description":"Token minted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StorefrontTokenResponse"}}}},"400":{"description":"Missing account identity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Invalid handshake","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"Handshake secret unset on this deployment. Carries error code `HANDSHAKE_NOT_CONFIGURED` — a permanent misconfiguration, not a transient outage; do not retry it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/size-fitting":{"post":{"tags":["Size Fitting"],"summary":"Estimate clothing size from a digital twin body profile","description":"Synchronously compare the private body profile of a measurements/clothing-size digital twin with body-range size charts.\n\nThis is an estimated, non-billable calculation. It supports new or backfilled twins created from `bodyMeasurements` or `clothingSize`; photo/direct twins return `available: false`. Size charts must use body measurement ranges, not garment dimensions. Products with no dimension available in both the profile and every size row are omitted; the response is unavailable only when none can be compared. The response contains fit statuses, bounded scores, and recommendations.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SizeFittingRequest"}}}},"responses":{"200":{"description":"Size fitting result (including unavailable profiles)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SizeFittingResponse"}}}},"400":{"description":"Invalid size chart or request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"413":{"description":"Request body exceeds 64 KiB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/pose-transfer":{"post":{"tags":["Pose Transfer"],"summary":"Transfer pose from one person to another","description":"Transfer a pose from a reference person (or cached pose) to a source person, generating an image of the source person in the new pose.\n\n## How it works\n\nThis endpoint takes a source person's image and applies a different pose to them while preserving their body shape and facial features.\n\n**Pipeline:**\n1. Extract 3D body mesh from source image using SAM-3D (~20s)\n2. Extract pose from reference image OR use cached pose (~0-20s)\n3. Apply pose transfer to source body mesh (~15s)\n4. Generate photorealistic image of source person in new pose (~30s)\n\n---\n\n## Using a Reference Pose Image\n\nProvide `poseImageUrl` to extract the pose from another person's photo.\n\n```json\n{\n  \"sourceImageUrl\": \"https://example.com/person-a.jpg\",\n  \"poseImageUrl\": \"https://example.com/person-b-pose.jpg\"\n}\n```\n\n**Requirements for pose reference:**\n- Full-body photo showing head to feet\n- Clear pose visible (standing, sitting, dancing, etc.)\n\n**Estimated time:** ~85 seconds\n\n---\n\n## Using a Cached Pose\n\nUse `poseId` for faster processing with pre-defined poses.\n\n```json\n{\n  \"sourceImageUrl\": \"https://example.com/person-a.jpg\",\n  \"poseId\": \"girl_pose\"\n}\n```\n\n**Available poses:**\n- `standing_arms_down`: Natural standing pose with arms relaxed at sides\n- `man_pose`: Cached pose extracted from assets/test/man-pose.jpg\n- `girl_pose`: Cached pose extracted from assets/test/girl-pose.jpg\n\n**Estimated time:** ~65 seconds\n\n---\n\n## Response\n\nReturns:\n- **Image**: Photorealistic image of the source person in the new pose\n- **GLB file** (optional): 3D body mesh with the transferred pose\n- **Visualization** (optional): Mesh visualization PNG\n- **digitalTwinId**: Cache key for reuse in try-on requests\n\nThe `digitalTwinId` can be used with `/api/v1/virtual-fitting` for instant try-ons without regenerating the twin.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PoseTransferRequest"}}}},"responses":{"201":{"description":"Job created successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PoseTransferResponse"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/digitization/batches":{"post":{"tags":["Digitization"],"summary":"Create a digitization batch","description":"\nSubmit up to 500 products for 3D model generation, with 1-10 image URLs per product. For bags and other products, set options.productType to other or auto.\nEach product can have multiple images - the best one will be selected automatically.\nUses the same processing logic as single shoe-3d requests (validation, AI correction, WEARFITS upload).\n\n**Key Features:**\n- Each product gets an external ID (productId) for easy tracking\n- Multiple images per product - best one selected automatically\n- AI image correction applied by default\n- Same quality as single shoe-3d requests\n- Batch options support productType, pipelineVariant, genaiQuality, correctImage, enhanceTexture, refineLogo, smoothNormals, skipCache and uploadToWearFits. Select one provider using options.model (singular); per-request shoe-3d fields such as glbInput and validateInQueue are not batch inputs.\n\n**Processing:**\n- Stage 1 (fal.ai 3D generation): up to 10 items processed concurrently\n- Stage 2 (Modal AI texture enhance + WEARFITS upload): up to 5 concurrent\n- Pipeline overlap: an item finishing stage-1 frees its slot for the next while stage-2 runs in parallel\n- Per-item processing time varies with provider load, cache availability and selected processing options\n- Batch traffic shares the stage-2 queue with real-time single-call /shoe-3d, so very large batches can backlog real-time stage-2\n\n**Webhook (batch.completed):**\n- Fires once when the last item reaches a terminal state (completed/failed)\n- Receivers must dedupe on `batchId` — a non-atomic lock makes near-simultaneous double-fire possible at the very tail\n- HMAC-SHA256 signature using the per-client `apiKeyHash`\n\n**Batch Limits:**\n- Maximum 500 products per batch\n- Maximum 10 images per product\n- Batches expire after 7 days\n\t","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"products":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"External product ID for tracking (returned in response)","example":"sku-123"},"images":{"type":"array","items":{"type":"string","format":"uri"},"minItems":1,"maxItems":10,"description":"Array of image URLs for this product (best selected automatically)","example":["https://example.com/shoe1-front.jpg","https://example.com/shoe1-side.jpg"]}},"required":["images"]},"minItems":1,"maxItems":500,"description":"Array of products to digitize into 3D models"},"options":{"type":"object","properties":{"uploadToWearFits":{"type":"boolean","default":true,"description":"Upload generated 3D models to WEARFITS for AR try-on"},"model":{"type":"string","description":"3D generation model to use (defaults to shoe-3d default model)"},"correctImage":{"type":"boolean","default":true,"description":"Apply AI image correction (Gemini packshot) before 3D generation"},"enhanceTexture":{"type":"boolean","default":true,"description":"Run AI texture enhancement after 3D generation: 6 views with genaiQuality high (default), or 4 with genaiQuality default."},"refineLogo":{"type":"boolean","default":false,"description":"Run a second AI pass focused on fixing logos/branding"},"smoothNormals":{"type":"boolean","default":true,"description":"Apply auto smooth normals (30° angle threshold) before WEARFITS upload. Disable when GLB is intended for Blender editing."},"pipelineVariant":{"type":"string","enum":["A","B"],"default":"A","description":"Hunyuan-3D pipeline variant. A = 50k tris (default). B = 100k tris simplified to 50k at the end (beta)."},"genaiQuality":{"type":"string","enum":["default","high"],"default":"high","description":"AI quality preset: \"default\" (4-view, 3-min timeout) or \"high\" (6-view, 5-min timeout)."},"productType":{"type":"string","enum":["shoe","other","auto"],"default":"shoe","description":"Product type for AI prompt scaffolding. \"shoe\" / \"other\" force the type, \"auto\" lets the validator detect it from the image (matches /api/v1/shoe-3d)."},"skipCache":{"type":"boolean","default":false,"description":"Bypass shoe-3d idempotency cache and force regeneration."}}},"webhookUrl":{"type":"string","format":"uri","description":"URL to call when batch completes (all items processed)","example":"https://your-app.com/webhooks/wearfits"}},"required":["products"]}}}},"responses":{"202":{"description":"Batch created and queued for processing","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[true]},"batch":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["pending","processing","completed","failed","paused"]},"progress":{"type":"object","properties":{"total":{"type":"number"},"completed":{"type":"number"},"failed":{"type":"number"},"pending":{"type":"number"}},"required":["total","completed","failed","pending"]},"createdAt":{"type":"string","format":"date-time"},"statusUrl":{"type":"string","description":"URL to check batch status","example":"/api/v1/digitization/batches/{batchId}"}},"required":["id","status","progress","createdAt","statusUrl"]}},"required":["success","batch"]}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/digitization/batches/{batchId}":{"get":{"tags":["Digitization"],"summary":"Get batch status","description":"Retrieve the current status and progress of a digitization batch.","parameters":[{"schema":{"type":"string","format":"uuid","description":"Batch ID"},"required":true,"name":"batchId","in":"path"}],"responses":{"200":{"description":"Batch details","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[true]},"batch":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["pending","processing","completed","failed","paused"]},"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"productId":{"type":"string","description":"External product ID provided by the client for tracking"},"images":{"type":"array","items":{"type":"string","format":"uri"},"description":"Array of image URLs for this product (best selected automatically)"},"status":{"type":"string","enum":["pending","queued","processing","completed","failed"]},"jobId":{"type":"string","format":"uuid","description":"Links to the shoe-3d job processing this item"},"glbUrl":{"type":"string"},"viewerUrl":{"type":"string"},"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]},"startedAt":{"type":"string","format":"date-time"},"completedAt":{"type":"string","format":"date-time"}},"required":["id","images","status"]}},"options":{"type":"object","properties":{"uploadToWearFits":{"type":"boolean","default":true,"description":"Upload generated 3D models to WEARFITS for AR try-on"},"model":{"type":"string","description":"3D generation model to use (defaults to shoe-3d default model)"},"correctImage":{"type":"boolean","default":true,"description":"Apply AI image correction (Gemini packshot) before 3D generation"},"enhanceTexture":{"type":"boolean","default":true,"description":"Run AI texture enhancement after 3D generation: 6 views with genaiQuality high (default), or 4 with genaiQuality default."},"refineLogo":{"type":"boolean","default":false,"description":"Run a second AI pass focused on fixing logos/branding"},"smoothNormals":{"type":"boolean","default":true,"description":"Apply auto smooth normals (30° angle threshold) before WEARFITS upload. Disable when GLB is intended for Blender editing."},"pipelineVariant":{"type":"string","enum":["A","B"],"default":"A","description":"Hunyuan-3D pipeline variant. A = 50k tris (default). B = 100k tris simplified to 50k at the end (beta)."},"genaiQuality":{"type":"string","enum":["default","high"],"default":"high","description":"AI quality preset: \"default\" (4-view, 3-min timeout) or \"high\" (6-view, 5-min timeout)."},"productType":{"type":"string","enum":["shoe","other","auto"],"default":"shoe","description":"Product type for AI prompt scaffolding. \"shoe\" / \"other\" force the type, \"auto\" lets the validator detect it from the image (matches /api/v1/shoe-3d)."},"skipCache":{"type":"boolean","default":false,"description":"Bypass shoe-3d idempotency cache and force regeneration."}}},"progress":{"type":"object","properties":{"total":{"type":"number"},"completed":{"type":"number"},"failed":{"type":"number"},"pending":{"type":"number"}},"required":["total","completed","failed","pending"]},"webhookUrl":{"type":"string","format":"uri","description":"URL to call when batch completes (all items processed)","example":"https://your-app.com/webhooks/wearfits"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"completedAt":{"type":"string","format":"date-time"},"apiKeyHash":{"type":"string"}},"required":["id","status","items","options","progress","createdAt","updatedAt"]}},"required":["success","batch"]}}}},"403":{"description":"Access denied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Batch not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/digitization/batches/{batchId}/pause":{"post":{"tags":["Digitization"],"summary":"Pause a batch","description":"Pause processing of a batch. Items already in progress will complete.","parameters":[{"schema":{"type":"string","format":"uuid","description":"Batch ID"},"required":true,"name":"batchId","in":"path"}],"responses":{"200":{"description":"Batch paused","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[true]},"status":{"type":"string","enum":["pending","processing","completed","failed","paused"]}},"required":["success","status"]}}}},"400":{"description":"Cannot pause batch","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Access denied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Batch not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/digitization/batches/{batchId}/resume":{"post":{"tags":["Digitization"],"summary":"Resume a paused batch","description":"Resume processing of a paused batch.","parameters":[{"schema":{"type":"string","format":"uuid","description":"Batch ID"},"required":true,"name":"batchId","in":"path"}],"responses":{"200":{"description":"Batch resumed","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[true]},"status":{"type":"string","enum":["pending","processing","completed","failed","paused"]}},"required":["success","status"]}}}},"400":{"description":"Cannot resume batch","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Access denied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Batch not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/jobs/{jobId}":{"get":{"tags":["Jobs"],"summary":"Get job status","description":"Retrieve the current status and results of a try-on job. Virtual-fitting and digital-twin progress is indeterminate until completed (100%). Use the submitting ownership context: API-key jobs require that same individual key, including during key rotation. Account-token and storefront-token jobs instead belong to the account within their respective authentication mode; a renewed token for the same account and mode retains access, subject to its route permissions. A different ownership context returns 404, identical to a missing job.","parameters":[{"schema":{"type":"string","format":"uuid","example":"550e8400-e29b-41d4-a716-446655440000"},"required":true,"name":"jobId","in":"path"}],"responses":{"200":{"description":"Job status retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobStatusResponse"}}}},"404":{"description":"Job not found, or not owned by the authenticated ownership context","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"tags":["Jobs"],"summary":"Cancel job","description":"Cancel a pending job. Only jobs in queued or validating status can be cancelled. API-key jobs require the submitting key. Token-owned jobs require the same account and authentication mode, and a credential permitted to cancel jobs. A different ownership context returns 404, identical to a missing job.","parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"jobId","in":"path"}],"responses":{"200":{"description":"Job cancelled successfully","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessResponse"},{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}]}}}},"400":{"description":"Job cannot be cancelled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Job not found, or not owned by the authenticated ownership context","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/jobs/{jobId}/trace":{"get":{"tags":["Jobs"],"summary":"Get job execution trace","description":"Retrieve detailed trace log for debugging pipeline failures. Shows timing, inputs, outputs, and retry attempts for each operation. Traces are retained for 7 days after job completion. API-key jobs require the submitting key. Token-owned jobs require the same account and authentication mode, and a credential permitted to read traces. A different ownership context returns 404, identical to a missing job. Upstream provider messages that describe OUR account state with that provider (billing, credits, key validity) are replaced with a neutral message before the trace is served; every other error string is verbatim.","parameters":[{"schema":{"type":"string","format":"uuid","example":"550e8400-e29b-41d4-a716-446655440000"},"required":true,"name":"jobId","in":"path"}],"responses":{"200":{"description":"Trace log retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TraceLogResponse"}}}},"404":{"description":"Job or trace not found, or the job is not owned by the authenticated ownership context","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/foot-measurement/upload-url":{"post":{"tags":["Foot Measurement"],"summary":"Reserve a job and get a presigned R2 PUT URL","description":"Returns a `jobId` plus a short-lived presigned URL the SPA can PUT the measurement zip to directly. After the PUT succeeds the SPA must call `POST /api/v1/foot-measurement` with the same `jobId` to trigger processing.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"sourceType":{"type":"string","enum":["webxr","marker"],"description":"Pose source — `webxr` for Android WebXR, `marker` for iOS card-based scans.","example":"webxr"}},"required":["sourceType"]}}}},"responses":{"201":{"description":"Job reserved; PUT the zip to `putUrl`.","content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"string","example":"fm-01HW..."},"putUrl":{"type":"string","format":"uri","description":"Presigned R2 PUT URL. PUT the measurement zip here with `Content-Type: application/zip`."},"expiresAt":{"type":"string","description":"ISO8601 expiry timestamp for the PUT URL."}},"required":["jobId","putUrl","expiresAt"]}}}},"400":{"description":"Invalid request body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"Presigned uploads are not configured on this deployment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/foot-measurement":{"post":{"tags":["Foot Measurement"],"summary":"Trigger processing for an uploaded scan","description":"Confirms that the SPA finished PUTting `measurement.zip` to R2, validates the zip contents, and enqueues a foot-measurement job. The two-step flow (reserve → PUT → trigger) bypasses the Cloudflare Workers 100 MB body cap.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"string","minLength":1,"description":"Job ID returned from POST /upload-url.","example":"fm-01HW..."},"sourceType":{"type":"string","enum":["webxr","marker"],"description":"Must match the value passed to POST /upload-url.","example":"webxr"}},"required":["jobId","sourceType"]}}}},"responses":{"201":{"description":"Job accepted and queued.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","example":"fm-01HW..."}},"required":["id"]}}}},"400":{"description":"Invalid request or zip contents.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Unknown jobId (never reserved or KV expired).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"No upload received yet for this jobId.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/foot-measurement/recent":{"get":{"tags":["Foot Measurement"],"summary":"List recent scan IDs (admin-only)","description":"Returns the most-recently-uploaded scan jobIds with timestamps and status. Requires the `X-Admin-Key` header to match the `FOOT_MEASUREMENT_ADMIN_KEY` secret. Never returns presigned URLs or content metadata — pure ID listing only. Returns 503 if the admin key is not configured.","security":[{"AdminKey":[]}],"parameters":[{"schema":{"type":"string","description":"Max scans to return (default 20, max 100).","example":"20"},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"List of recent scans.","content":{"application/json":{"schema":{"type":"object","properties":{"scans":{"type":"array","items":{"type":"object","properties":{"jobId":{"type":"string","example":"fm-40d017ae-0d49-4b94-8093-060f1ced8f42"},"createdAt":{"type":"string","example":"2026-05-28T07:29:40Z"},"status":{"type":"string","enum":["awaiting-upload","pending","processing","done","error","unknown"],"example":"done"},"clientId":{"type":"string","description":"Pseudonymous client/device ID (from meta.json `client_id`, sanitized). Groups repeat scans from one device. Absent for pre-client-id uploads or expired KV records.","example":"0f8fad5b-d9cb-46d2-a1e4-426655440000"}},"required":["jobId","createdAt","status"]}}},"required":["scans"]}}}},"401":{"description":"Missing or invalid X-Admin-Key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"Admin listing is not configured on this deployment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/foot-measurement/{id}":{"get":{"tags":["Foot Measurement"],"summary":"Get foot-measurement job status","description":"Returns the current state of a foot-measurement job. When `status === \"done\"` the response also carries `resultUrl`, a presigned R2 GET URL for `result.json` that expires in **10 minutes** (600s). Clients should re-poll this endpoint to obtain a fresh URL rather than caching it.","parameters":[{"schema":{"type":"string","example":"fm-01HW..."},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Job status (and result URL if done).","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["awaiting-upload","pending","processing","done","error"]},"resultUrl":{"type":"string","format":"uri","description":"Presigned R2 GET URL for `result.json`. Valid for **10 minutes** (600s). Re-poll `GET /api/v1/foot-measurement/{id}` to obtain a fresh URL — do not cache or share the link."},"error":{"type":"string"}},"required":["id","status"]}}}},"404":{"description":"Job not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"tags":["Foot Measurement"],"summary":"Delete a foot-measurement job and its artifacts (admin-only)","description":"Admin-only destructive cleanup. Requires `X-Admin-Key` matching `FOOT_MEASUREMENT_ADMIN_KEY` — the same gate as `/recent`. Without admin auth users have no way to delete arbitrary jobs by jobId; expired blobs are still swept by the 24h R2 lifecycle rule + KV TTL.","security":[{"AdminKey":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Deletion accepted.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"deleted":{"type":"boolean","enum":[true]}},"required":["id","deleted"]}}}},"401":{"description":"Missing or invalid X-Admin-Key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No KV record AND no R2 input.zip for this jobId — nothing to delete.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"Admin deletion is not configured on this deployment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/texture-painter/api/enhance":{"post":{"tags":["Texture Painter"],"summary":"AI texture enhancement","description":"Enhance a 3D model texture render using AI and reference packshot images. Accepts base64 data URL images only. Uses OpenRouter (Gemini) to generate an enhanced texture that maintains the original silhouette.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TextureEnhanceRequest"}}}},"responses":{"200":{"description":"Enhanced texture image","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TextureEnhanceResponse"}}}}}}},"/texture-painter/api/split-materials":{"post":{"summary":"Split GLB materials by UV quadrants (Async)","description":"Starts an async job to divide a GLB mesh primitives into quadrants based on GenAI semantic mapping.","tags":["Texture Painter"],"requestBody":{"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"glb":{"description":"The GLB file to split materials for","format":"binary"},"textureType":{"type":"string","enum":["baseColorTexture","metallicRoughnessTexture","normalTexture","emissiveTexture","occlusionTexture","none"],"description":"The texture channel to use for SAM2 segmentation / K-Means clustering (if none or absent, uses 4 quadrants)"},"method":{"type":"string","enum":["multiview_ai"],"default":"multiview_ai","description":"The algorithm method to use for splitting materials."}},"required":["glb"]}}}},"responses":{"202":{"description":"Job Accepted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"jobId":{"type":"string"}},"required":["success","jobId"]}}}},"400":{"description":"Bad Request"},"500":{"description":"Internal Server Error"}}}},"/texture-painter/api/status/{jobId}":{"get":{"summary":"Get split materials job status","tags":["Texture Painter"],"parameters":[{"schema":{"type":"string"},"required":true,"name":"jobId","in":"path"}],"responses":{"200":{"description":"Job Status","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["processing","completed","failed"]},"resultUrl":{"type":"string"},"error":{"type":"string"}},"required":["status"]}}}}}}}}}