{"openapi":"3.1.0","info":{"title":"Phota API","description":"Create and manage profiles for photo editing and enhancement.","version":"v0.7.0"},"paths":{"/v1/phota/edit":{"post":{"tags":["Studio"],"summary":"Edit","description":"Edit images based on a text prompt.\n\nProvide input images (base64 or public URL) and a prompt describing the\ndesired edit. Returns base64 images or signed download URLs.","operationId":"edit_v1_phota_edit_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EditRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EditResponse"}}}},"401":{"description":"Invalid API key in the X-API-Key header."},"403":{"description":"Missing X-API-Key header (no API key provided)."},"402":{"description":"Insufficient prepaid credit balance."},"400":{"description":"Invalid request, content moderation violation, or profiles of mixed model versions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhotaError400Response"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhotaError404Response"}}}},"422":{"description":"No image was produced (not charged, retryable), or request validation failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhotaError422Response"}}}},"500":{"description":"Internal error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhotaError500Response"}}}},"503":{"description":"Service temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhotaError503Response"}}}},"504":{"description":"Synchronous request exceeded the 600s limit; resubmit with `async: true`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhotaError504Response"}}}},"202":{"description":"Async job accepted; poll job_id at poll_url.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobHandle"}}}}},"security":[{"APIKeyHeader":[]}]}},"/v1/phota/generate":{"post":{"tags":["Studio"],"summary":"Generate","description":"Generate images from a text prompt without input images.\n\nProvide a prompt describing the desired image. Returns base64 images or\nsigned download URLs.","operationId":"generate_v1_phota_generate_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EditResponse"}}}},"401":{"description":"Invalid API key in the X-API-Key header."},"403":{"description":"Missing X-API-Key header (no API key provided)."},"402":{"description":"Insufficient prepaid credit balance."},"400":{"description":"Invalid request, content moderation violation, or profiles of mixed model versions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhotaError400Response"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhotaError404Response"}}}},"422":{"description":"No image was produced (not charged, retryable), or request validation failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhotaError422Response"}}}},"500":{"description":"Internal error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhotaError500Response"}}}},"503":{"description":"Service temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhotaError503Response"}}}},"504":{"description":"Synchronous request exceeded the 600s limit; resubmit with `async: true`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhotaError504Response"}}}},"202":{"description":"Async job accepted; poll job_id at poll_url.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobHandle"}}}}},"security":[{"APIKeyHeader":[]}]}},"/v1/phota/enhance":{"post":{"tags":["Studio"],"summary":"Enhance","description":"Enhance a single image.\n\nEnhancement parameters are inferred automatically. Optionally provide\na text prompt to guide the enhancement. Returns images or signed download URLs.","operationId":"enhance_v1_phota_enhance_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnhanceRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EditResponse"}}}},"401":{"description":"Invalid API key in the X-API-Key header."},"403":{"description":"Missing X-API-Key header (no API key provided)."},"402":{"description":"Insufficient prepaid credit balance."},"400":{"description":"Invalid request, content moderation violation, or profiles of mixed model versions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhotaError400Response"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhotaError404Response"}}}},"422":{"description":"No image was produced (not charged, retryable), or request validation failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhotaError422Response"}}}},"500":{"description":"Internal error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhotaError500Response"}}}},"503":{"description":"Service temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhotaError503Response"}}}},"504":{"description":"Synchronous request exceeded the 600s limit; resubmit with `async: true`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhotaError504Response"}}}},"202":{"description":"Async job accepted; poll job_id at poll_url.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobHandle"}}}}},"security":[{"APIKeyHeader":[]}]}},"/v1/phota/remix":{"post":{"tags":["Studio"],"summary":"Remix","description":"Restyle an image to match a reference image.\n\nProvide an input image and a reference image; the output keeps the\ninput's content rendered in the reference's style. Optional profile IDs\npreserve recognized identities, and an optional prompt steers the remix.\nOutput resolution is fixed at 2K. Returns base64 images or signed\ndownload URLs.","operationId":"remix_v1_phota_remix_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemixRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EditResponse"}}}},"401":{"description":"Invalid API key in the X-API-Key header."},"403":{"description":"Missing X-API-Key header (no API key provided)."},"402":{"description":"Insufficient prepaid credit balance."},"400":{"description":"Invalid request, content moderation violation, or profiles of mixed model versions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhotaError400Response"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhotaError404Response"}}}},"422":{"description":"No image was produced (not charged, retryable), or request validation failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhotaError422Response"}}}},"500":{"description":"Internal error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhotaError500Response"}}}},"503":{"description":"Service temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhotaError503Response"}}}},"504":{"description":"Synchronous request exceeded the 600s limit; resubmit with `async: true`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhotaError504Response"}}}},"202":{"description":"Async job accepted; poll job_id at poll_url.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobHandle"}}}}},"security":[{"APIKeyHeader":[]}]}},"/v1/phota/jobs/{job_id}":{"get":{"tags":["Studio"],"summary":"Get Job Status","description":"Poll an async edit/generate/enhance/remix job's status.","operationId":"get_job_status_v1_phota_jobs__job_id__get","security":[{"APIKeyHeader":[]}],"parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","title":"Job Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobStatus"}}}},"401":{"description":"Invalid API key in the X-API-Key header."},"403":{"description":"Missing X-API-Key header (no API key provided)."},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhotaError404Response"}}}},"410":{"description":"Job result has expired."},"500":{"description":"Internal error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhotaError500Response"}}}},"503":{"description":"Service temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhotaError503Response"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/phota/webhook-secret":{"get":{"tags":["Studio"],"summary":"Get Webhook Secret","description":"Return the authenticated developer's webhook signing secret.\n\nThis is the per-developer HMAC key used to sign the ``X-Phota-Signature``\nheader on async job callbacks; partners fetch it once to verify callback\nauthenticity. The secret is minted lazily on first use. Scoped strictly to\nthe authenticated developer (the API key yields the developer uid).","operationId":"get_webhook_secret_v1_phota_webhook_secret_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookSecretResponse"}}}},"401":{"description":"Invalid API key in the X-API-Key header."},"403":{"description":"Missing X-API-Key header (no API key provided)."}},"security":[{"APIKeyHeader":[]}]}},"/v1/phota/profiles/ids":{"get":{"tags":["Profiles"],"summary":"Get Profile Ids","description":"List all profiles for the authenticated user, optionally filtered by tag.","operationId":"get_profile_ids_v1_phota_profiles_ids_get","security":[{"APIKeyHeader":[]}],"parameters":[{"name":"tag","in":"query","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":128},{"type":"null"}],"description":"Filter profiles by tag. Omit to return all profiles.","title":"Tag"},"description":"Filter profiles by tag. Omit to return all profiles."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileIdsResponse"}}}},"401":{"description":"Invalid API key in the X-API-Key header."},"403":{"description":"Missing X-API-Key header (no API key provided)."},"402":{"description":"Insufficient prepaid credit balance."},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhotaError404Response"}}}},"500":{"description":"Internal error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhotaError500Response"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/phota/profiles/tags":{"post":{"tags":["Profiles"],"summary":"Tag Profiles","description":"Backfill tags onto existing profiles you own.\n\nSets each submitted tag only where the profile has no tag yet; existing\ntags are never overwritten. Every entry lands in exactly one response\nbucket, so partial application is normal and resubmitting the same\nmapping is safe.","operationId":"tag_profiles_v1_phota_profiles_tags_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TagProfilesRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TagProfilesResponse"}}}},"401":{"description":"Invalid API key in the X-API-Key header."},"403":{"description":"Missing X-API-Key header (no API key provided)."},"422":{"description":"No image was produced (not charged, retryable), or request validation failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhotaError422Response"}}}},"500":{"description":"Internal error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhotaError500Response"}}}}},"security":[{"APIKeyHeader":[]}]}},"/v1/phota/profiles/{profile_id}/status":{"get":{"tags":["Profiles"],"summary":"Get Profile Status","description":"Get the status of a profile.","operationId":"get_profile_status_v1_phota_profiles__profile_id__status_get","security":[{"APIKeyHeader":[]}],"parameters":[{"name":"profile_id","in":"path","required":true,"schema":{"type":"string","title":"Profile Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileStatusResponse"}}}},"401":{"description":"Invalid API key in the X-API-Key header."},"403":{"description":"Missing X-API-Key header (no API key provided)."},"402":{"description":"Insufficient prepaid credit balance."},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhotaError404Response"}}}},"500":{"description":"Internal error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhotaError500Response"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/phota/profiles/{profile_id}/profile_picture":{"get":{"tags":["Profiles"],"summary":"Get Profile Picture","description":"Return the profile picture as raw JPEG image bytes.","operationId":"get_profile_picture_v1_phota_profiles__profile_id__profile_picture_get","security":[{"APIKeyHeader":[]}],"parameters":[{"name":"profile_id","in":"path","required":true,"schema":{"type":"string","title":"Profile Id"}}],"responses":{"200":{"description":"Profile picture as JPEG image.","content":{"image/jpeg":{}}},"401":{"description":"Invalid API key in the X-API-Key header."},"403":{"description":"Missing X-API-Key header (no API key provided)."},"402":{"description":"Insufficient prepaid credit balance."},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhotaError404Response"}}}},"500":{"description":"Internal error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhotaError500Response"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/phota/profiles/add":{"post":{"tags":["Profiles"],"summary":"Add Profile","description":"Create a new profile from signed image URLs.\n\nStarts training asynchronously and returns a profile ID immediately.\nPoll `GET /profiles/{profile_id}/status` to check when training completes.","operationId":"add_profile_v1_phota_profiles_add_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddProfileRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddProfileResponse"}}}},"401":{"description":"Invalid API key in the X-API-Key header."},"403":{"description":"Missing X-API-Key header (no API key provided)."},"402":{"description":"Insufficient prepaid credit balance."},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhotaError404Response"}}}},"500":{"description":"Internal error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhotaError500Response"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"APIKeyHeader":[]}]}},"/v1/phota/profiles/{profile_id}":{"delete":{"tags":["Profiles"],"summary":"Delete Profile","description":"Permanently delete a profile and all associated data.","operationId":"delete_profile_v1_phota_profiles__profile_id__delete","security":[{"APIKeyHeader":[]}],"parameters":[{"name":"profile_id","in":"path","required":true,"schema":{"type":"string","title":"Profile Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteProfileResponse"}}}},"401":{"description":"Invalid API key in the X-API-Key header."},"403":{"description":"Missing X-API-Key header (no API key provided)."},"402":{"description":"Insufficient prepaid credit balance."},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhotaError404Response"}}}},"500":{"description":"Internal error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhotaError500Response"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"AddProfileRequest":{"properties":{"image_urls":{"items":{"type":"string"},"type":"array","maxItems":50,"minItems":5,"title":"Image Urls","description":"Publicly accessible URLs pointing to the training images. Supported formats: JPEG, PNG, WebP, and HEIC/HEIF. Images larger than 4K (4096px) are automatically resized; for best results, provide images at 4K or below. PNG images are transcoded to JPEG (quality 95) for storage efficiency. Image-count range depends on training_tier: \"standard\" requires 10–50, \"fast\" requires 5–10."},"training_tier":{"$ref":"#/components/schemas/TrainingTier","description":"Training tier for the profile. \"standard\" trains on 10–50 images for highest quality; \"fast\" trains on 5–10 images for quicker turnaround at lower cost.","default":"standard"},"tag":{"anyOf":[{"type":"string","maxLength":128,"minLength":1},{"type":"null"}],"title":"Tag","description":"Optional immutable tag to group or namespace this profile (e.g., your end-user's identifier)."},"model_version":{"anyOf":[{"$ref":"#/components/schemas/ProfileModelVersion"},{"type":"null"}],"description":"Personalization model version to train this profile on: \"v1\" (original) or \"v2\" (current). Omit to use your account's default (v2 for new accounts). Profiles of different model versions cannot be combined in one /edit, /generate, /enhance, or /remix request."}},"type":"object","required":["image_urls"],"title":"AddProfileRequest","description":"Request body for creating a new profile.","examples":[{"image_urls":["https://example.com/photo1.jpg","https://storage.googleapis.com/bucket/photo2.jpg?X-Goog-Signature=..."],"tag":"user_abc123"},{"image_urls":["https://example.com/photo1.jpg","https://example.com/photo2.jpg","https://example.com/photo3.jpg","https://example.com/photo4.jpg","https://example.com/photo5.jpg","https://example.com/photo6.jpg"],"tag":"user_abc123","training_tier":"fast"},{"image_urls":["https://example.com/photo1.jpg","https://example.com/photo2.jpg","https://example.com/photo3.jpg","https://example.com/photo4.jpg","https://example.com/photo5.jpg","https://example.com/photo6.jpg","https://example.com/photo7.jpg","https://example.com/photo8.jpg","https://example.com/photo9.jpg","https://example.com/photo10.jpg"],"model_version":"v1","tag":"user_abc123"}]},"AddProfileResponse":{"properties":{"profile_id":{"type":"string","title":"Profile Id","description":"Unique identifier for the new profile. Use this to poll training status."}},"type":"object","required":["profile_id"],"title":"AddProfileResponse","description":"Response returned after a profile is created."},"DeleteProfileResponse":{"properties":{"profile_id":{"type":"string","title":"Profile Id","description":"Identifier of the deleted profile."},"status":{"type":"string","title":"Status","description":"Result status, e.g. 'ok'."}},"type":"object","required":["profile_id","status"],"title":"DeleteProfileResponse","description":"Response confirming a profile has been permanently deleted."},"EditJobErrorCode":{"type":"string","enum":["instance_died","dispatch_failed"],"title":"EditJobErrorCode","description":"Job-infrastructure failure codes with no synchronous-pipeline equivalent.\n\nPipeline failures reuse the synchronous customer-facing taxonomy\n(``api.schema.phota.PhotaErrorCode``) so an async failure surfaces the same\nstable code as its sync twin. These members cover the job-infrastructure\nfailure modes the sync path never produces:\n\nAttributes:\n    INSTANCE_DIED: A claimed (``RUNNING``) job did not complete: the run_job\n        handler's attempt ceiling fired, the run failed after the claim, or\n        the instance died mid-run and the sweep reaped it.\n    DISPATCH_FAILED: No Cloud Tasks delivery ever claimed a ``PENDING`` job\n        within the sweep's abandoned window."},"EditRequest":{"properties":{"async":{"type":"boolean","title":"Async","description":"Submit asynchronously. Returns 202 with a job_id to poll at GET /v1/jobs/{job_id} instead of holding the connection until the image is ready.","default":false},"callback_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Callback Url","description":"Optional https URL we POST the signed result to on completion. Best-effort (polling remains authoritative). Requires async=true."},"client_request_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Request Id","description":"Idempotency key. A retried submit with the same value returns the original job instead of creating (and charging for) a new one."},"response_mode":{"type":"string","enum":["bytes","urls"],"title":"Response Mode","description":"Response delivery mode. 'bytes': return base64-encoded image data (default). 'urls': return signed download URLs (24-hour expiry) instead of image bytes.","default":"bytes"},"prompt":{"type":"string","title":"Prompt","description":"Text prompt describing the desired edit.","default":""},"images":{"items":{"type":"string"},"type":"array","maxItems":10,"title":"Images","description":"Input images as raw base64 strings or publicly accessible URLs. Both formats are auto-detected. Supported formats: JPEG, PNG, WebP, and HEIC/HEIF. Images larger than 4K (4096px) are automatically resized; for best results, provide images at 4K or below."},"profile_ids":{"items":{"type":"string"},"type":"array","title":"Profile Ids","description":"Subset of your account's profiles relevant to this request. Your account holds all profiles, but only pass the ones belonging to the end-user whose photo is being edited. These profiles serve as candidates for identity preservation. The fewer profiles you pass, the easier it is for our system to determine which profiles are relevant."},"num_output_images":{"type":"integer","maximum":4.0,"minimum":1.0,"title":"Num Output Images","description":"Number of output images to generate (1-4).","default":1},"aspect_ratio":{"anyOf":[{"type":"string","enum":["auto","1:1","3:4","4:3","9:16","16:9"]},{"type":"null"}],"title":"Aspect Ratio","description":"Output aspect ratio (1:1, 3:4, 4:3, 9:16, 16:9; 'auto' matches the input image where supported). Omit to use the model's default: 'auto' for models that support it, '4:3' otherwise. 'auto' is rejected with 400 for models that do not support it."},"resolution":{"type":"string","enum":["1K","2K","4K"],"title":"Resolution","description":"Output resolution (1K, 2K, 4K). Per-model support varies; see /models.","default":"1K"},"quality":{"anyOf":[{"type":"string","enum":["auto","low","medium","high"]},{"type":"null"}],"title":"Quality","description":"Quality tier (auto, low, medium, high). Only supported by gpt-image-2; passing this field for other models returns 400. Omit (or pass 'auto') to let the provider pick — for gpt-image-2 this resolves to OpenAI's auto tier. Lower tiers reduce output tokens and therefore settled cost."},"base_model":{"type":"string","enum":["flux-2","flux-2-pro","gpt-image-2","nb2","qwen-image-2","seedream-5-pro"],"title":"Base Model","description":"Base model identifier. Omit to use the default base model. Unknown ids are rejected with 400; base models that do not support this endpoint's capability are also rejected with 400.","default":"nb2"},"output_format":{"type":"string","enum":["png","jpg"],"title":"Output Format","description":"Output image format (png, jpg). Default: png (will change to jpg on 2026-05-08).","default":"png"}},"type":"object","title":"EditRequest","description":"Request body for the edit endpoint.\n\nInput images can be provided as either raw base64-encoded bytes or\npublicly accessible URLs (e.g. ``https://example.com/photo.jpg``).\nBoth formats are accepted and detected automatically.","examples":[{"images":["iVBORw0KGgo...","https://example.com/photo.jpg"],"num_output_images":2,"profile_ids":["abc123","def456"],"prompt":"Turn [[def456]] into a vampire"}]},"EditResponse":{"properties":{"images":{"items":{"type":"string"},"type":"array","title":"Images","description":"Output image(s) as raw base64-encoded strings. Format matches the requested output_format. Populated when response_mode='bytes', empty otherwise."},"download_urls":{"items":{"type":"string"},"type":"array","title":"Download Urls","description":"Signed download URLs for each output image (24-hour expiry). Populated when response_mode='urls', empty otherwise."},"known_subjects":{"$ref":"#/components/schemas/KnownGeneratedSubjectCounts","description":"Dictionary mapping a known subject's `profile_id` to the number of times they were generated. If multiple variations are generated, this will be the aggregated count across all variations."}},"type":"object","required":["known_subjects"],"title":"EditResponse","description":"Response returned by the edit, generate, and enhance endpoints."},"EnhanceRequest":{"properties":{"async":{"type":"boolean","title":"Async","description":"Submit asynchronously. Returns 202 with a job_id to poll at GET /v1/jobs/{job_id} instead of holding the connection until the image is ready.","default":false},"callback_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Callback Url","description":"Optional https URL we POST the signed result to on completion. Best-effort (polling remains authoritative). Requires async=true."},"client_request_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Request Id","description":"Idempotency key. A retried submit with the same value returns the original job instead of creating (and charging for) a new one."},"response_mode":{"type":"string","enum":["bytes","urls"],"title":"Response Mode","description":"Response delivery mode. 'bytes': return base64-encoded image data (default). 'urls': return signed download URLs (24-hour expiry) instead of image bytes.","default":"bytes"},"image":{"type":"string","title":"Image","description":"Input image as a raw base64 string or publicly accessible URL. Supported formats: JPEG, PNG, WebP, and HEIC/HEIF. Images larger than 4K (4096px) are automatically resized; for best results, provide images at 4K or below."},"profile_ids":{"items":{"type":"string"},"type":"array","title":"Profile Ids","description":"Subset of your account's profiles relevant to this request. Your account holds all profiles, but only pass the ones belonging to the end-user whose photo is being edited. These profiles serve as candidates for identity preservation. The fewer profiles you pass, the easier it is for our system to determine which profiles are relevant."},"num_output_images":{"type":"integer","maximum":4.0,"minimum":1.0,"title":"Num Output Images","description":"Number of output images to generate (1-4).","default":1},"output_format":{"type":"string","enum":["png","jpg"],"title":"Output Format","description":"Output image format (png, jpg). Default: png (will change to jpg on 2026-05-08).","default":"png"},"prompt":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Prompt","description":"Optional text instructions to guide the enhancement."}},"type":"object","required":["image"],"title":"EnhanceRequest","description":"Request body for the enhance endpoint.\n\nEnhances the provided input image without requiring a text prompt,\naspect ratio, or resolution -- those parameters are inferred automatically.\n\nThe image can be provided as a raw base64-encoded string or a publicly\naccessible URL. Both formats are accepted and detected automatically.","examples":[{"image":"https://example.com/photo.jpg","num_output_images":1,"profile_ids":["abc123"]}]},"GenerateRequest":{"properties":{"async":{"type":"boolean","title":"Async","description":"Submit asynchronously. Returns 202 with a job_id to poll at GET /v1/jobs/{job_id} instead of holding the connection until the image is ready.","default":false},"callback_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Callback Url","description":"Optional https URL we POST the signed result to on completion. Best-effort (polling remains authoritative). Requires async=true."},"client_request_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Request Id","description":"Idempotency key. A retried submit with the same value returns the original job instead of creating (and charging for) a new one."},"response_mode":{"type":"string","enum":["bytes","urls"],"title":"Response Mode","description":"Response delivery mode. 'bytes': return base64-encoded image data (default). 'urls': return signed download URLs (24-hour expiry) instead of image bytes.","default":"bytes"},"prompt":{"type":"string","title":"Prompt","description":"Text prompt describing the desired edit.","default":""},"num_output_images":{"type":"integer","maximum":4.0,"minimum":1.0,"title":"Num Output Images","description":"Number of output images to generate (1-4).","default":1},"aspect_ratio":{"anyOf":[{"type":"string","enum":["auto","1:1","3:4","4:3","9:16","16:9"]},{"type":"null"}],"title":"Aspect Ratio","description":"Output aspect ratio (1:1, 3:4, 4:3, 9:16, 16:9; 'auto' matches the input image where supported). Omit to use the model's default: 'auto' for models that support it, '4:3' otherwise. 'auto' is rejected with 400 for models that do not support it."},"resolution":{"type":"string","enum":["1K","2K","4K"],"title":"Resolution","description":"Output resolution (1K, 2K, 4K). Per-model support varies; see /models.","default":"1K"},"quality":{"anyOf":[{"type":"string","enum":["auto","low","medium","high"]},{"type":"null"}],"title":"Quality","description":"Quality tier (auto, low, medium, high). Only supported by gpt-image-2; passing this field for other models returns 400. Omit (or pass 'auto') to let the provider pick — for gpt-image-2 this resolves to OpenAI's auto tier. Lower tiers reduce output tokens and therefore settled cost."},"base_model":{"type":"string","enum":["flux-2","flux-2-pro","gpt-image-2","nb2","qwen-image-2","seedream-5-pro"],"title":"Base Model","description":"Base model identifier. Omit to use the default base model. Unknown ids are rejected with 400; base models that do not support this endpoint's capability are also rejected with 400.","default":"nb2"},"output_format":{"type":"string","enum":["png","jpg"],"title":"Output Format","description":"Output image format (png, jpg). Default: png (will change to jpg on 2026-05-08).","default":"png"}},"type":"object","title":"GenerateRequest","description":"Request body for the generate endpoint.\n\nProvide a text prompt describing the image you want to create.\nReference profiles inline using ``[[profile_id]]`` syntax to\nspecify which people should appear in the generated image.","examples":[{"num_output_images":1,"prompt":"Generate a photo of [[abc123]] on a tropical beach"}]},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"JobError":{"properties":{"code":{"anyOf":[{"$ref":"#/components/schemas/PhotaErrorCode"},{"$ref":"#/components/schemas/EditJobErrorCode"}],"title":"Code","description":"Machine-readable error code (a PhotaErrorCode or job-infra EditJobErrorCode)."},"message":{"type":"string","title":"Message","description":"Human-readable error description."}},"type":"object","required":["code","message"],"title":"JobError","description":"Failure detail surfaced on a failed job.\n\n``code`` is one of the synchronous customer-facing categories\n(:class:`PhotaErrorCode`, so an async pipeline failure surfaces the same\nstable code as its sync twin) or a job-infrastructure-specific code\n(:class:`~database.models.EditJobErrorCode`, for the watchdog reap /\ndispatch-exhaustion states that have no synchronous equivalent)."},"JobHandle":{"properties":{"job_id":{"type":"string","title":"Job Id","description":"Opaque job identifier. Poll GET /v1/jobs/{job_id} for status."},"status":{"type":"string","const":"pending","title":"Status","description":"Always 'pending' at submit; transitions to running then succeeded/failed.","default":"pending"},"poll_url":{"type":"string","title":"Poll Url","description":"Relative path to poll for job status, e.g. '/v1/jobs/<job_id>'."}},"type":"object","required":["job_id","poll_url"],"title":"JobHandle","description":"202 response when an edit/generate/enhance/remix is submitted with async=true."},"JobStatus":{"properties":{"job_id":{"type":"string","title":"Job Id"},"status":{"type":"string","enum":["pending","running","succeeded","failed"],"title":"Status"},"operation":{"type":"string","enum":["edit","generate","enhance","remix"],"title":"Operation"},"result":{"anyOf":[{"$ref":"#/components/schemas/EditResponse"},{"type":"null"}],"description":"The edit/generate result (download_urls populated) once status is 'succeeded'."},"error":{"anyOf":[{"$ref":"#/components/schemas/JobError"},{"type":"null"}],"description":"Populated once status is 'failed'."},"created_at":{"type":"string","format":"date-time","title":"Created At"},"completed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Completed At"}},"type":"object","required":["job_id","status","operation","created_at"],"title":"JobStatus","description":"Status of an async edit/generate job (GET /v1/jobs/{job_id})."},"KnownGeneratedSubjectCounts":{"properties":{"counts":{"additionalProperties":{"type":"integer"},"type":"object","title":"Counts","description":"Mapping of profile ID to the number of times that subject was generated."}},"type":"object","title":"KnownGeneratedSubjectCounts","description":"Counts of known subjects generated, keyed by profile ID."},"PhotaError400Response":{"properties":{"detail":{"type":"string","title":"Detail","description":"Human-readable error message."},"code":{"type":"string","enum":["CONTENT_MODERATION","INVALID_REQUEST","MIXED_PROFILE_MODEL_VERSIONS"],"title":"Code","description":"Machine-readable error code."},"request_id":{"type":"string","title":"Request Id","description":"Unique request identifier for support."}},"type":"object","required":["detail","code","request_id"],"title":"PhotaError400Response","description":"Error response for 400 Bad Request."},"PhotaError404Response":{"properties":{"detail":{"type":"string","title":"Detail","description":"Human-readable error message."},"code":{"type":"string","enum":["NOT_FOUND","PROFILE_NOT_FOUND","PROFILE_NOT_READY"],"title":"Code","description":"Machine-readable error code."},"request_id":{"type":"string","title":"Request Id","description":"Unique request identifier for support."}},"type":"object","required":["detail","code","request_id"],"title":"PhotaError404Response","description":"Error response for 404 Not Found."},"PhotaError422Response":{"properties":{"detail":{"type":"string","title":"Detail","description":"Human-readable error message."},"code":{"type":"string","enum":["NO_IMAGE_GENERATED","INVALID_REQUEST"],"title":"Code","description":"Machine-readable error code."},"request_id":{"type":"string","title":"Request Id","description":"Unique request identifier for support."}},"type":"object","required":["detail","code","request_id"],"title":"PhotaError422Response","description":"Error response for 422 Unprocessable Content.\n\nCovers the model completing without producing an image (``NO_IMAGE_GENERATED``,\nnot charged, retryable) and request-body validation failures (``INVALID_REQUEST``)."},"PhotaError500Response":{"properties":{"detail":{"type":"string","title":"Detail","description":"Human-readable error message."},"code":{"type":"string","const":"INTERNAL_ERROR","title":"Code","description":"Machine-readable error code."},"request_id":{"type":"string","title":"Request Id","description":"Unique request identifier for support."}},"type":"object","required":["detail","code","request_id"],"title":"PhotaError500Response","description":"Error response for 500 Internal Server Error."},"PhotaError503Response":{"properties":{"detail":{"type":"string","title":"Detail","description":"Human-readable error message."},"code":{"type":"string","enum":["SERVICE_UNAVAILABLE","INPUT_IMAGE_FETCH_FAILED"],"title":"Code","description":"Machine-readable error code."},"request_id":{"type":"string","title":"Request Id","description":"Unique request identifier for support."}},"type":"object","required":["detail","code","request_id"],"title":"PhotaError503Response","description":"Error response for 503 Service Unavailable."},"PhotaError504Response":{"properties":{"detail":{"type":"string","title":"Detail","description":"Human-readable error message."},"code":{"type":"string","const":"TIMEOUT","title":"Code","description":"Machine-readable error code."},"request_id":{"type":"string","title":"Request Id","description":"Unique request identifier for support."}},"type":"object","required":["detail","code","request_id"],"title":"PhotaError504Response","description":"Error response for 504 Gateway Timeout (synchronous time budget exceeded)."},"PhotaErrorCode":{"type":"string","enum":["CONTENT_MODERATION","SERVICE_UNAVAILABLE","INPUT_IMAGE_FETCH_FAILED","NOT_FOUND","PROFILE_NOT_FOUND","PROFILE_NOT_READY","INVALID_REQUEST","NO_IMAGE_GENERATED","TIMEOUT","INTERNAL_ERROR","MIXED_PROFILE_MODEL_VERSIONS"],"title":"PhotaErrorCode","description":"Machine-readable error codes returned by the Phota API.\n\nClients can use these codes to programmatically react to specific error\ntypes without parsing human-readable messages.\n\nAttributes:\n    CONTENT_MODERATION: Content was blocked by a safety filter.\n    SERVICE_UNAVAILABLE: Upstream resource exhaustion or rate limit.\n    INPUT_IMAGE_FETCH_FAILED: A caller-supplied input image URL could not\n        be downloaded after retries because the URL's host/CDN kept\n        failing (timeouts or 5xx from the image host). Retryable, but the\n        failure is upstream of Phota — the URL's host, not this service.\n    NOT_FOUND: Generic resource not found or no longer available (unknown\n        route, unknown job, or an expired job result).\n    PROFILE_NOT_FOUND: The requested profile does not exist.\n    PROFILE_NOT_READY: The profile exists but training has not completed.\n    INVALID_REQUEST: Malformed input, missing fields, or bad parameters.\n    NO_IMAGE_GENERATED: The model completed without producing an image (a\n        soft refusal or transient glitch — no safety verdict). The request\n        is not charged; retrying may succeed.\n    TIMEOUT: The request exceeded its synchronous time budget; resubmit\n        with ``async: true`` and poll for the result.\n    INTERNAL_ERROR: An unexpected internal error (genuine bug).\n    MIXED_PROFILE_MODEL_VERSIONS: The request references profiles trained on\n        different model versions (``v1`` and ``v2``); a request may only use\n        profiles of one model version."},"ProfileIdsResponse":{"properties":{"profiles":{"items":{"$ref":"#/components/schemas/ProfileInfo"},"type":"array","title":"Profiles","description":"List of profile summaries. Empty if the user has no profiles."}},"type":"object","required":["profiles"],"title":"ProfileIdsResponse","description":"Response containing all profiles owned by the authenticated user."},"ProfileInfo":{"properties":{"profile_id":{"type":"string","title":"Profile Id","description":"The profile identifier."},"status":{"$ref":"#/components/schemas/ProfileStatus","description":"Current status of the profile."},"tag":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tag","description":"Tag associated with this profile, if any."}},"type":"object","required":["profile_id","status"],"title":"ProfileInfo","description":"Summary information for a single profile."},"ProfileModelVersion":{"type":"string","enum":["v1","v2"],"title":"ProfileModelVersion","description":"Personalization model version a profile is trained on.\n\nAttributes:\n    V1: The original personalization model.\n    V2: The current personalization model."},"ProfileStatus":{"type":"string","enum":["VALIDATING","QUEUING","IN_PROGRESS","READY","ERROR","INACTIVE"],"title":"ProfileStatus","description":"Status of a profile."},"ProfileStatusResponse":{"properties":{"profile_id":{"type":"string","title":"Profile Id","description":"The queried profile identifier."},"status":{"$ref":"#/components/schemas/ProfileStatus","description":"Current status of the profile."},"message":{"type":"string","title":"Message","description":"Optional message in case of an error."}},"type":"object","required":["profile_id","status","message"],"title":"ProfileStatusResponse","description":"Response containing the current status of a profile."},"ProfileTagEntry":{"properties":{"profile_id":{"type":"string","maxLength":64,"minLength":1,"title":"Profile Id","description":"Identifier of the profile to tag."},"tag":{"type":"string","maxLength":128,"minLength":1,"title":"Tag","description":"Tag to set (e.g., your end-user's identifier). Immutable once set."}},"type":"object","required":["profile_id","tag"],"title":"ProfileTagEntry","description":"One profile -> tag assignment in a tag-backfill request."},"RemixRequest":{"properties":{"async":{"type":"boolean","title":"Async","description":"Submit asynchronously. Returns 202 with a job_id to poll at GET /v1/jobs/{job_id} instead of holding the connection until the image is ready.","default":false},"callback_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Callback Url","description":"Optional https URL we POST the signed result to on completion. Best-effort (polling remains authoritative). Requires async=true."},"client_request_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Request Id","description":"Idempotency key. A retried submit with the same value returns the original job instead of creating (and charging for) a new one."},"response_mode":{"type":"string","enum":["bytes","urls"],"title":"Response Mode","description":"Response delivery mode. 'bytes': return base64-encoded image data (default). 'urls': return signed download URLs (24-hour expiry) instead of image bytes.","default":"bytes"},"input_image":{"type":"string","title":"Input Image","description":"Input image as a raw base64 string or publicly accessible URL. Supported formats: JPEG, PNG, WebP, and HEIC/HEIF. Images larger than 4K (4096px) are automatically resized."},"reference_image":{"type":"string","title":"Reference Image","description":"Reference image as a raw base64 string or publicly accessible URL. The output keeps the input image's content rendered in this image's style."},"profile_ids":{"items":{"type":"string"},"type":"array","title":"Profile Ids","description":"Profile IDs for identity preservation in the output. When a listed profile is recognized in the input image, the person's likeness is preserved through the restyle. Optional.","default":[]},"prompt":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Prompt","description":"Optional text instructions to steer the remix (e.g. what to keep or emphasize)."},"num_output_images":{"type":"integer","maximum":4.0,"minimum":1.0,"title":"Num Output Images","description":"Number of output images to generate (1-4).","default":1},"output_format":{"type":"string","enum":["png","jpg"],"title":"Output Format","description":"Output image format (png, jpg). Default: png (will change to jpg on 2026-05-08).","default":"png"}},"type":"object","required":["input_image","reference_image"],"title":"RemixRequest","description":"Request body for the remix endpoint.","examples":[{"input_image":"https://example.com/photo.jpg","num_output_images":2,"output_format":"jpg","prompt":"Keep the original background","reference_image":"https://example.com/reference.jpg","response_mode":"urls"}]},"TagConflict":{"properties":{"profile_id":{"type":"string","title":"Profile Id","description":"The profile that could not be tagged."},"existing_tag":{"type":"string","title":"Existing Tag","description":"The tag the profile already carries."}},"type":"object","required":["profile_id","existing_tag"],"title":"TagConflict","description":"A profile that already carries a different tag; it was left untouched."},"TagProfilesRequest":{"properties":{"tags":{"items":{"$ref":"#/components/schemas/ProfileTagEntry"},"type":"array","maxItems":1000,"minItems":1,"title":"Tags","description":"Profile-to-tag assignments to apply. Each profile may appear at most once."}},"type":"object","required":["tags"],"title":"TagProfilesRequest","description":"Request body for backfilling tags onto existing untagged profiles.","examples":[{"tags":[{"profile_id":"0ec2f38b1b5143de83f443266baa0027","tag":"user_abc123"},{"profile_id":"f75b1445f7274a94836102a2ae4d67d5","tag":"user_def456"}]}]},"TagProfilesResponse":{"properties":{"updated":{"type":"integer","title":"Updated","description":"Number of profiles whose tag was set by this request."},"already_tagged":{"items":{"type":"string"},"type":"array","title":"Already Tagged","description":"Profiles that already carried the exact submitted tag (idempotent no-op)."},"conflicts":{"items":{"$ref":"#/components/schemas/TagConflict"},"type":"array","title":"Conflicts","description":"Profiles that already carry a different tag; existing tags are never overwritten."},"not_found":{"items":{"type":"string"},"type":"array","title":"Not Found","description":"Profile ids that do not exist, are deleted, or are not owned by you."}},"type":"object","required":["updated","already_tagged","conflicts","not_found"],"title":"TagProfilesResponse","description":"Per-outcome report for a tag-backfill request.\n\nEvery requested profile lands in exactly one bucket, so\n``updated + len(already_tagged) + len(conflicts) + len(not_found)``\nequals the number of submitted entries."},"TrainingTier":{"type":"string","enum":["standard","fast"],"title":"TrainingTier","description":"Training tier for a profile job.\n\nAttributes:\n    STANDARD: Full-quality training (10-50 images, 500-1000 steps scaled by image count).\n    FAST: Reduced-image, faster-turnaround training (5-10 images, 150-300 steps scaled by image count)."},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"WebhookSecretResponse":{"properties":{"webhook_signing_secret":{"type":"string","title":"Webhook Signing Secret","description":"Per-developer HMAC key for verifying the X-Phota-Signature on job callbacks."}},"type":"object","required":["webhook_signing_secret"],"title":"WebhookSecretResponse","description":"The authenticated developer's webhook signing secret (GET .../webhook-secret).\n\nThe same per-developer secret used to HMAC-sign the ``X-Phota-Signature``\nheader on async job callbacks. Use it to verify a callback's authenticity:\nrecompute ``sha256`` HMAC over the exact received body and compare."}},"securitySchemes":{"APIKeyHeader":{"type":"apiKey","in":"header","name":"X-API-Key"}}},"tags":[{"name":"Studio","description":"Image editing, generation, and enhancement."},{"name":"Profiles","description":"Profile lifecycle: create, query, delete."}],"servers":[{"url":"https://api.photalabs.com"}]}