# Asynchronous requests Source: https://docs.photalabs.com/api/async-requests Submit long-running edits as jobs, poll for the result, and receive optional signed callbacks By default the image endpoints (`/edit`, `/generate`, `/enhance`, `/remix`) are **synchronous**: the connection stays open until the result is ready. That's the simplest option for fast, interactive requests. **Asynchronous mode** decouples the result from the HTTP connection. Add `async: true` and the call returns immediately with a `job_id`; the work runs in the background and you fetch the result by polling — with an optional signed callback when it finishes. Your client submits the job and moves on instead of holding a connection open for the full duration of the edit. That means less overhead on your side and no dependency on a long-lived connection staying up — a natural fit for serverless functions, job queues, and other environments where keeping a request in flight is costly or impractical. Async jobs also get a larger runtime budget than the synchronous path, so longer edits have room to finish. **Polling is authoritative.** `GET /v2/phota/jobs/{job_id}` is always the source of truth for a job's outcome. A `callback_url` is a best-effort convenience on top of polling, not a replacement for it. ## When to use it * **You don't want to hold a connection open** for the duration of the work — submit the job and poll (or receive a callback) when it's ready, with no in-flight request to keep alive. * **You're integrating from a serverless, queued, or otherwise connection-constrained environment** where a long-lived request is costly or impractical. * **You want a durable handle** to poll or be notified on completion, decoupled from any single request. * **Long-running edits** that run in the background instead of tying up a connection while they finish. For fast, interactive requests the synchronous path remains simpler — keep using it without `async`. ## Request fields These optional fields are accepted on every image endpoint (`/edit`, `/generate`, `/enhance`, `/remix`): | Field | Type | Notes | | - | - | - | | `async` | bool | Opt in to asynchronous mode. Default `false` (unchanged synchronous behavior). | | `callback_url` | string | Optional. HTTPS URL POSTed once the job finishes. Only valid when `async: true`. | | `client_request_id` | string | Optional idempotency key — a retried submit with the same value returns the original job instead of a new one. | Async results are always delivered as download URLs, so `async: true` requires `response_mode: "urls"`. Combining `async: true` with `response_mode: "bytes"` returns `422`. A `callback_url` without `async: true` also returns `422`. Add `async: true` and `response_mode: "urls"` to any image request. The endpoint returns `202 Accepted` with a job handle instead of the image payload: ```bash curl theme={null} curl -X POST https://api.photalabs.com/v2/phota/edit \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "prompt": "Make [[abc123]] smile wider", "images": ["https://example.com/photo.jpg"], "profile_ids": ["abc123"], "num_output_images": 2, "output_format": "jpg", "response_mode": "urls", "async": true }' ``` ```python Python theme={null} import requests resp = requests.post( "https://api.photalabs.com/v2/phota/edit", headers={"X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json"}, json={ "prompt": "Make [[abc123]] smile wider", "images": ["https://example.com/photo.jpg"], "profile_ids": ["abc123"], "num_output_images": 2, "output_format": "jpg", "response_mode": "urls", "async": True, }, ) job = resp.json() # HTTP 202 print(f"Job submitted: {job['job_id']}") ``` The `202` response is a job handle: ```json theme={null} { "job_id": "5f3c8a1e9b4d4c7e8a2f1b6d0c9e7a31", "status": "pending", "poll_url": "/v2/phota/jobs/5f3c8a1e9b4d4c7e8a2f1b6d0c9e7a31" } ``` Poll `GET /v2/phota/jobs/{job_id}` until `status` is `succeeded` or `failed`: ```bash curl theme={null} curl https://api.photalabs.com/v2/phota/jobs/5f3c8a1e9b4d4c7e8a2f1b6d0c9e7a31 \ -H "X-API-Key: YOUR_API_KEY" ``` ```python Python theme={null} import time import requests headers = {"X-API-Key": "YOUR_API_KEY"} job_id = "5f3c8a1e9b4d4c7e8a2f1b6d0c9e7a31" while True: resp = requests.get( f"https://api.photalabs.com/v2/phota/jobs/{job_id}", headers=headers, ) job = resp.json() if job["status"] == "succeeded": print(job["result"]["download_urls"]) break if job["status"] == "failed": print(f"Job failed: {job['error']['code']} — {job['error']['message']}") break time.sleep(3) ``` A succeeded job carries the same result shape as the synchronous response, with `download_urls` populated (`images` is always empty in async mode): ```json theme={null} { "job_id": "5f3c8a1e9b4d4c7e8a2f1b6d0c9e7a31", "status": "succeeded", "operation": "edit", "result": { "images": [], "download_urls": [ "https://cache-cdn.photalabs.com/20260622/abc123.jpg?token=...&expires=...", "https://cache-cdn.photalabs.com/20260622/def456.jpg?token=...&expires=..." ], "known_subjects": { "counts": { "abc123": 2 } } }, "error": null, "created_at": "2026-06-22T12:00:00Z", "completed_at": "2026-06-22T12:00:18Z" } ``` | `status` | Meaning | | - | - | | `pending` | Accepted, not started yet. Poll again shortly. | | `running` | Executing. Poll again shortly. | | `succeeded` | Done — `result.download_urls` is populated. | | `failed` | Failed — `error.code` and `error.message` explain why. | Results are pollable for **24 hours** (matching the signed-URL lifetime). After that the job is garbage-collected and polling returns `410 Gone`. Polling a job that belongs to another account returns `404` (jobs are scoped to the account that submitted them). Pass a `callback_url` on submit and Phota POSTs the **same payload as the poll response** to that URL once the job reaches a terminal state. Delivery is best-effort with a few bounded retries — always treat polling as authoritative. Every callback is signed so you can verify it came from Phota. The body is HMAC-SHA256-signed with your per-account **webhook signing secret**, sent in the `X-Phota-Signature` header as `sha256=`. Fetch your secret once from: ```bash theme={null} curl https://api.photalabs.com/v2/phota/webhook-secret \ -H "X-API-Key: YOUR_API_KEY" ``` ```json theme={null} { "webhook_signing_secret": "a1b2c3..." } ``` Verify a received callback by recomputing the HMAC over the **raw request body** and comparing in constant time: ```python Python theme={null} import hashlib import hmac def verify_callback(raw_body: bytes, signature_header: str, secret: str) -> bool: expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, signature_header) ``` Sign and compare over the **exact bytes** you received, before any JSON re-serialization — re-encoding can change the bytes and break the signature. ## Idempotency Pass a `client_request_id` to make submits safe to retry. If a submit with the same `client_request_id` is received again (for example after a network blip), the API returns the **original** job handle instead of creating — or charging for — a second job. ## Billing Async jobs are billed exactly like their synchronous equivalents: the floor cost is reserved at submit, settled to the final amount when the job **succeeds**, and released (refunded) if the job **fails**. As with the synchronous path, you pay for work performed — a job you submit and never poll still runs and settles. ## Errors | Status | When | | - | - | | `422` | `async: true` with `response_mode: "bytes"`, or `callback_url` without `async: true`. | | `402` | Insufficient prepaid balance at submit — no job is created. | | `404` | Polling an unknown job, or one belonging to another account. | | `410` | Polling a job past its 24-hour retention window. | ## Next steps The synchronous workflow, end to end. Full request and response schemas for every endpoint. # Authentication Source: https://docs.photalabs.com/api/authentication Authenticate your API requests with an API key All Phota API endpoints require authentication via the `X-API-Key` header. ## Obtaining an API key Sign up or log in to the Developer Portal to generate an API key. ## Using your API key Include the `X-API-Key` header in every request: ```bash curl theme={null} curl https://api.photalabs.com/v2/phota/ \ -H "X-API-Key: YOUR_API_KEY" ``` ```python Python theme={null} import requests resp = requests.get( "https://api.photalabs.com/v2/phota/", headers={"X-API-Key": "YOUR_API_KEY"}, ) ``` ## Error responses If the API key is missing or invalid, the API returns a `401 Unauthorized` response: ```json theme={null} { "detail": "Missing or invalid API key in the X-API-Key header." } ``` ## Best practices Never commit API keys to version control or expose them in client-side code. Use environment variables or a secrets manager to store them. Store your key in an environment variable and reference it in your code: ```bash theme={null} export PHOTA_API_KEY="your-key-here" ``` ```python theme={null} import os import requests api_key = os.environ["PHOTA_API_KEY"] resp = requests.get( "https://api.photalabs.com/v2/phota/", headers={"X-API-Key": api_key}, ) ``` Generate a new key from the Developer Portal and update your applications. Revoke old keys once the new key is deployed. # Add Profile Source: https://docs.photalabs.com/api/profiles/add-profile https://api.photalabs.com/v2/phota/openapi.json post /v2/phota/profiles/add Create a new profile from signed image URLs. Starts training asynchronously and returns a profile ID immediately. Poll `GET /profiles/{profile_id}/status` to check when training completes. # Delete Profile Source: https://docs.photalabs.com/api/profiles/delete-profile https://api.photalabs.com/v2/phota/openapi.json delete /v2/phota/profiles/{profile_id} Permanently delete a profile and all associated data. # Get Profile Ids Source: https://docs.photalabs.com/api/profiles/get-profile-ids https://api.photalabs.com/v2/phota/openapi.json get /v2/phota/profiles/ids List all profiles for the authenticated user, optionally filtered by tag. # Get Profile Picture Source: https://docs.photalabs.com/api/profiles/get-profile-picture https://api.photalabs.com/v2/phota/openapi.json get /v2/phota/profiles/{profile_id}/profile_picture Return the profile picture as raw JPEG image bytes. # Get Profile Status Source: https://docs.photalabs.com/api/profiles/get-profile-status https://api.photalabs.com/v2/phota/openapi.json get /v2/phota/profiles/{profile_id}/status Get the status of a profile. # Tag Profiles Source: https://docs.photalabs.com/api/profiles/tag-profiles https://api.photalabs.com/v2/phota/openapi.json post /v2/phota/profiles/tags Backfill tags onto existing profiles you own. Sets each submitted tag only where the profile has no tag yet; existing tags are never overwritten. Every entry lands in exactly one response bucket, so partial application is normal and resubmitting the same mapping is safe. # Quickstart Source: https://docs.photalabs.com/api/quickstart Go from zero to your first edited image in five minutes This guide walks you through the core Phota API workflow: authenticate, create a profile, wait for training, and run your first edit, generation, and enhancement. **Prerequisites**: You need an API key from the Developer Portal. All requests require the `X-API-Key` header. Every request must include your API key in the `X-API-Key` header. Verify your key works by listing your profiles: ```bash curl theme={null} curl https://api.photalabs.com/v2/phota/profiles/ids \ -H "X-API-Key: YOUR_API_KEY" ``` ```python Python theme={null} import requests resp = requests.get( "https://api.photalabs.com/v2/phota/profiles/ids", headers={"X-API-Key": "YOUR_API_KEY"}, ) print(resp.json()) ``` You should receive a JSON response with your profiles (empty if you haven't created any yet): ```json theme={null} { "profiles": [] } ``` A profile represents a subject whose identity you want to preserve across edits and generations. Provide to publicly accessible image URLs of the subject (person, dog, or cat): ```bash curl theme={null} curl -X POST https://api.photalabs.com/v2/phota/profiles/add \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "image_urls": [ "https://example.com/photo1.jpg", "https://example.com/photo2.jpg", "https://example.com/photo3.jpg" ], "tag": "user_abc123" }' ``` ```python Python theme={null} import requests resp = requests.post( "https://api.photalabs.com/v2/phota/profiles/add", headers={ "X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json", }, json={ "image_urls": [ "https://example.com/photo1.jpg", "https://example.com/photo2.jpg", "https://example.com/photo3.jpg", ], "tag": "user_abc123", }, ) profile_id = resp.json()["profile_id"] print(f"Profile created: {profile_id}") ``` The response includes the new profile ID: ```json theme={null} { "profile_id": "abc123" } ``` Training starts immediately and runs asynchronously. The profile is not ready for use until training completes. See the [Profiles guide](/guides/profiles) for tips on choosing good training photos. Check the profile status until it reaches `READY`: ```bash curl theme={null} curl https://api.photalabs.com/v2/phota/profiles/abc123/status \ -H "X-API-Key: YOUR_API_KEY" ``` ```python Python theme={null} import time import requests headers = {"X-API-Key": "YOUR_API_KEY"} while True: resp = requests.get( "https://api.photalabs.com/v2/phota/profiles/abc123/status", headers=headers, ) data = resp.json() print(f"Status: {data['status']}") if data["status"] == "READY": break elif data["status"] == "ERROR": print(f"Training failed: {data['message']}") break time.sleep(10) ``` Possible status values: | Status | Meaning | | - | - | | `IN_PROGRESS` | Training is still running. Poll again in a few seconds. | | `READY` | Training completed. The profile is ready for edits. | | `ERROR` | Training failed. Check the `message` field for details. | Once the profile is `READY`, you can reference it in an edit request. Provide input images as base64 strings or public URLs, along with a text prompt. You can refer to a profile inline in the prompt with `[[profile_id]]` and/or list candidate profiles in the `profile_ids` field — either mechanism is enough on its own: ```bash curl theme={null} curl -X POST https://api.photalabs.com/v2/phota/edit \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "prompt": "Make [[abc123]] smile wider", "images": ["https://example.com/photo.jpg"], "profile_ids": ["abc123"], "aspect_ratio": "1:1", "resolution": "4K", "num_output_images": 2, "output_format": "jpg" }' ``` ```python Python theme={null} import requests resp = requests.post( "https://api.photalabs.com/v2/phota/edit", headers={ "X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json", }, json={ "prompt": "Make [[abc123]] smile wider", "images": ["https://example.com/photo.jpg"], "profile_ids": ["abc123"], "aspect_ratio": "1:1", "resolution": "4K", "num_output_images": 2, "output_format": "jpg", }, ) data = resp.json() # Default response_mode is "bytes" — images are base64-encoded strings print(f"Received {len(data['images'])} image(s)") print(f"Known subjects: {data['known_subjects']['counts']}") ``` This example shows both mechanisms for illustration. In practice you'd pick one — see the [Profiles guide](/guides/profiles#referencing-profiles-in-requests) for when each approach is best. The response contains the output images as base64-encoded strings (default `response_mode: "bytes"`): ```json theme={null} { "images": ["/9j/4AAQ...", "/9j/4AAQ..."], "download_urls": [], "known_subjects": { "counts": { "abc123": 2 } } } ``` You can control the output aspect ratio (e.g. `"9:16"`), resolution (up to `"4K"`), and `output_format` (`"png"` or `"jpg"`) on every request. Set `response_mode: "urls"` to receive signed CDN download URLs instead of base64 bytes. Add `"base_model": "gpt-image-2"` (or another supported value) to pick which image model powers the request. Defaults to `"nb2"` (Nano Banana 2). See [Base model selection](/guides/base-models). To get download URLs instead of base64 bytes, set `response_mode: "urls"`: ```bash curl theme={null} curl -X POST https://api.photalabs.com/v2/phota/edit \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "prompt": "Make [[abc123]] smile wider", "images": ["https://example.com/photo.jpg"], "profile_ids": ["abc123"], "resolution": "4K", "num_output_images": 2, "output_format": "jpg", "response_mode": "urls" }' ``` ```json theme={null} { "images": [], "download_urls": [ "https://cache-cdn.photalabs.com/20260408/abc123.jpg?token=...&expires=...", "https://cache-cdn.photalabs.com/20260408/def456.jpg?token=...&expires=..." ], "known_subjects": { "counts": { "abc123": 2 } } } ``` You can also generate images from scratch -- no input image needed. Reference profiles directly in the prompt using `[[profile_id]]` syntax: ```bash curl theme={null} curl -X POST https://api.photalabs.com/v2/phota/generate \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "prompt": "A professional portrait shot of [[abc123]] on a tropical beach at sunset", "num_output_images": 2, "output_format": "jpg" }' ``` ```python Python theme={null} import requests resp = requests.post( "https://api.photalabs.com/v2/phota/generate", headers={ "X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json", }, json={ "prompt": "A professional portrait shot of [[abc123]] on a tropical beach at sunset", "num_output_images": 2, "output_format": "jpg", }, ) data = resp.json() print(f"Received {len(data['images'])} image(s)") ``` The response format is the same as `/edit`: ```json theme={null} { "images": ["/9j/4AAQ...", "/9j/4AAQ..."], "download_urls": [], "known_subjects": { "counts": { "abc123": 2 } } } ``` Generation is non-deterministic — request multiple variations with `num_output_images` and let the user pick the best one. The `/generate` endpoint does not accept `images` or `profile_ids`. Reference profiles in the prompt with `[[profile_id]]` syntax instead. If you need to transform an existing photo, use `/edit`. `/generate` also accepts `base_model` — same set of values as `/edit`, same default of `"nb2"`. See [Base model selection](/guides/base-models). Enhancement improves a photo automatically -- no prompt required: ```bash curl theme={null} curl -X POST https://api.photalabs.com/v2/phota/enhance \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "image": "https://example.com/photo.jpg", "profile_ids": ["abc123"], "num_output_images": 2, "output_format": "jpg" }' ``` ```python Python theme={null} import requests resp = requests.post( "https://api.photalabs.com/v2/phota/enhance", headers={ "X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json", }, json={ "image": "https://example.com/photo.jpg", "profile_ids": ["abc123"], "num_output_images": 2, "output_format": "jpg", }, ) data = resp.json() print(f"Received {len(data['images'])} image(s)") print(f"Known subjects: {data['known_subjects']['counts']}") ``` `/enhance` also accepts an optional `prompt` with text instructions to guide the enhancement. When omitted, enhancement parameters are inferred automatically. `/enhance` does not accept `base_model`. Enhancement is pinned to Nano Banana 2 at 2K — see [Pricing](/pricing). ## Next steps Submit long-running edits as jobs and poll (or get a callback) for the result. Learn about API key management and best practices. See detailed examples of each capability with visual previews. Deep-dive into profiles, identity preservation, and prompt syntax. Per-image and per-profile rates. # Edit Source: https://docs.photalabs.com/api/studio/edit https://api.photalabs.com/v2/phota/openapi.json post /v2/phota/edit Edit images based on a text prompt. Provide input images (base64 or public URL) and a prompt describing the desired edit. Returns base64 images or signed download URLs. # Enhance Source: https://docs.photalabs.com/api/studio/enhance https://api.photalabs.com/v2/phota/openapi.json post /v2/phota/enhance Enhance a single image. Enhancement parameters are inferred automatically. Optionally provide a text prompt to guide the enhancement. Returns images or signed download URLs. # Generate Source: https://docs.photalabs.com/api/studio/generate https://api.photalabs.com/v2/phota/openapi.json post /v2/phota/generate Generate images from a text prompt without input images. Provide a prompt describing the desired image. Returns base64 images or signed download URLs. # Get Job Status Source: https://docs.photalabs.com/api/studio/get-job-status https://api.photalabs.com/v2/phota/openapi.json get /v2/phota/jobs/{job_id} Poll an async edit/generate/enhance/remix job's status. # Get Webhook Secret Source: https://docs.photalabs.com/api/studio/get-webhook-secret https://api.photalabs.com/v2/phota/openapi.json get /v2/phota/webhook-secret Return the authenticated developer's webhook signing secret. This is the per-developer HMAC key used to sign the ``X-Phota-Signature`` header on async job callbacks; partners fetch it once to verify callback authenticity. The secret is minted lazily on first use. Scoped strictly to the authenticated developer (the API key yields the developer uid). # Remix Source: https://docs.photalabs.com/api/studio/remix https://api.photalabs.com/v2/phota/openapi.json post /v2/phota/remix Restyle an image to match a reference image. Provide an input image and a reference image; the output keeps the input's content rendered in the reference's style. Optional profile IDs preserve recognized identities, and an optional prompt steers the remix. Output resolution is fixed at 2K. Returns base64 images or signed download URLs. # Base model selection Source: https://docs.photalabs.com/guides/base-models Choose which image model powers your /edit and /generate requests — identity preservation composes on top of any supported model ## Identity is a layer that has to be learned. Generic image models such as Nano Banana 2 and 2.1, GPT Image 2 and 2.5, Qwen Image 2, Flux 2 Dev, Flux 2 Pro, and Seedream 5.0 Pro have different strengths: world knowledge, text rendering, aesthetic defaults, cost profile. They've all shipped in roughly a year and the frontier keeps moving. What every one of them is missing is **personal knowledge of a specific person.** That's what Phota's identity layer adds. The identity layer composes on top of whichever base model you pick — so you choose the generic model that best fits your use case, and Phota handles identity for you. For more background, see [Every Frontier Model Can Be Personalized for You](https://photalabs.com/blog/every-frontier-model-can-be-personalized-for-you). You select the base model per request with the `base_model` parameter. Identity preservation is applied automatically whenever a trained profile is referenced. *** ## Supported models | `base_model` | Display name | Resolutions | Pricing | | - | - | - | - | | `nb2` (default) | Nano Banana 2 | 1K, 2K, 4K | flat per image | | `nb2.1` | Nano Banana 2.1 | 1K, 2K, 4K | flat per image | | `gpt-image-2` | GPT Image 2 | 1K, 2K, 4K | token-metered | | `gpt-image-2.5-sunburst` | GPT Image 2.5 Sunburst | 1K, 2K, 4K | token-metered | | `gpt-image-2.5-flare` | GPT Image 2.5 Flare | 1K, 2K, 4K | token-metered | | `qwen-image-2` | Qwen Image 2 | 1K, 2K | flat per image | | `flux-2` | Flux 2 Dev | 1K, 2K | flat per image | | `flux-2-pro` | Flux 2 Pro | 1K, 2K | megapixel-metered | | `seedream-5-pro` | Seedream 5.0 Pro | 1K, 2K | flat per image | See [Pricing](/pricing) for the exact rates. If you omit `base_model`, requests run on Nano Banana 2. We plan to make Nano Banana 2.1 (`nb2.1`) the default base model in a future release. To keep running on Nano Banana 2 after that, pass `base_model: "nb2"` explicitly. *** ## Where `base_model` applies * **`/edit`** — accepts `base_model`. Defaults to `nb2`. * **`/generate`** — accepts `base_model`. Defaults to `nb2`. * **`/enhance`** — does **not** accept `base_model`. Enhancement is pinned to Nano Banana 2 at 2K. *** ## Custom resolution (GPT Image models only) On `/edit` and `/generate`, set `custom_resolution` to an exact `"WIDTHxHEIGHT"` string: ```json theme={null} { "prompt": "A watercolor of a blue cup on a yellow table", "base_model": "gpt-image-2.5-flare", "quality": "xhigh", "custom_resolution": "1536x864" } ``` Both dimensions must be multiples of 16, neither may exceed 3840 pixels, and the longer edge must be at most three times the shorter edge. The pixel count must be between 655,360 and 8,294,400, inclusive. These are OpenAI's own limits, so any size accepted here is one the model will render. `"3840x2160"` and `"2880x2880"` sit exactly on the ceiling; `"3840x2176"` is over it, and `"4096x1376"` exceeds the edge limit. Sizes above 3,686,400 pixels are reserved at the 4K rate and settle on actual usage, like every other size. Omit `resolution` and `aspect_ratio` when using `custom_resolution`. The custom size replaces the default `"1K"` tier; combining it with `"2K"`, `"4K"`, or any explicit `aspect_ratio` is rejected. Other base models reject `custom_resolution` with HTTP 400. The existing resolution presets remain available, including their larger sizes. Custom sizes also work with `async: true` and `response_mode: "urls"`. ## Quality tier (GPT Image models only) `/edit` and `/generate` accept an optional `quality` parameter — `"auto"`, `"low"`, `"medium"`, or `"high"` — when `base_model` is `gpt-image-2`, `gpt-image-2.5-sunburst`, or `gpt-image-2.5-flare`. Omitting it lands on `"auto"`. `gpt-image-2.5-sunburst` and `gpt-image-2.5-flare` also accept `"xhigh"` and `"max"`. GPT Image 2 rejects these two tiers with HTTP 400. Sending `quality` with any other base model returns HTTP 400. Billing is token-driven, so lower tiers produce fewer output tokens and cost less per request. *** ## Example: pick a base model explicitly ```bash curl theme={null} curl -X POST https://api.photalabs.com/v2/phota/edit \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "prompt": "Make [[abc123]] smile wider", "images": ["https://example.com/photo.jpg"], "base_model": "gpt-image-2", "resolution": "2K", "output_format": "jpg" }' ``` ```python Python theme={null} import requests resp = requests.post( "https://api.photalabs.com/v2/phota/edit", headers={ "X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json", }, json={ "prompt": "Make [[abc123]] smile wider", "images": ["https://example.com/photo.jpg"], "base_model": "gpt-image-2", "resolution": "2K", "output_format": "jpg", }, ) print(resp.json()) ``` *** ## Next steps Per-image and token-metered rates for every supported base model. Create and manage profiles for identity preservation. Walk through the full workflow with code examples. # Best practices Source: https://docs.photalabs.com/guides/best-practices Tips for getting the best results from the Phota API *** ## Choose the right training photos Profile quality directly affects every edit and generation you run. Aim for – reference photos that cover a range of conditions: * **Clear, well-lit faces** -- the subject's face should be easy to see in every photo. * **High resolution** -- use 1K+ images. Avoid small crops, heavy compression, or noisy shots. * **Diverse expressions** -- include neutral, smiling, frowning, and surprised faces. * **Multiple angles** -- front-facing, slight left/right turns, and at least one profile view. * **Varied lighting** -- mix indoor, outdoor, sunny, and overcast conditions. * **Group shots are OK** -- as long as the intended subject's face is clearly visible. Avoid extreme poses, motion blur, heavy occlusion (sunglasses covering most of the face), and dark or noisy images. See the [Profiles guide](/guides/profiles#tips-for-best-results) for visual examples of good and bad training photos. *** ## Scope `profile_ids` to the current end-user The API only considers profiles for identify preservation that are explicitly included in each request. It never silently matches against other profiles in your account -- if you don't pass any, no identity preservation is applied. Your developer account holds profiles for all of your users, but each request should only include the profiles belonging to the end-user whose photo is being processed. A profile can be passed via the `profile_ids` field **or** inline with `[[profile_id]]` in the prompt — you don't need both. If you do list the same ID in both places it's harmless (the API merges them), but usually one is enough. See [Referencing profiles in requests](/guides/profiles#referencing-profiles-in-requests) for when each is best. ```json theme={null} { "prompt": "Add [[abc123]] to the family picture", "images": ["https://example.com/family.jpg"], "profile_ids": ["def456", "ghi789", "jkl012"] } ``` Here `[[abc123]]` is picked up from the prompt and the other three IDs are additional candidates in case the API needs to recognize anyone already in the input photo. The fewer profiles you pass, the easier it is for our system to match the right ones. Passing your entire account's profile list forces the model to sort through unrelated identities, which can hurt accuracy. *** ## Write descriptive prompts The more specific your prompt, the better the result. Two principles help: 1. **Say what to change.** Be explicit about the transformation you want -- pose, expression, background, style, lighting. 2. **Say what to keep.** Tell the model what should stay the same so it doesn't alter things you care about. ```json theme={null} { "prompt": "Make [[abc123]] cross their arms and look directly at the camera. Keep the background, lighting, and outfit unchanged.", "images": ["https://example.com/photo.jpg"], "profile_ids": ["abc123"] } ``` Vague prompts like "make it better" give the model too much freedom. Instead, describe the specific change: "Brighten the lighting on the subject's face and remove the shadow on the left side." *** ## Use `[[profile_id]]` in prompts for generation and multi-person scenes For edit and enhance requests, the `profile_ids` field tells the API which identities are available to draw from. The `[[profile_id]]` prompt syntax helps referring to a specific subject in the input photo. **When you need it:** * **Generation** -- the `/generate` endpoint does not accept `profile_ids`. Inline `[[...]]` references in the prompt are the only way to specify which subjects to generate. **Generation example:** ```json theme={null} { "prompt": "A photo of [[abc123]] and [[def456]] sitting across from each other at a cafe", "num_output_images": 2 } ``` **Edit example:** ```json theme={null} { "prompt": "Add [[def456]] to this group shot", "images": ["https://example.com/group.jpg"], "profile_ids": ["abc123","ghi789"] } ``` For single-person edits where the subject is already in the input photo, `profile_ids` alone is usually enough -- our system matches the face in the photo to the provided profiles automatically. For edits, profile IDs referenced with `[[...]]` in the prompt are automatically merged into `profile_ids`, so you don't need to list them in both places (but it doesn't hurt if you do). *** ## Use aspect ratio and resolution controls You can set `aspect_ratio` and `resolution` on every edit and generate request. Use these when: * You need a specific aspect ratio (e.g. `9:16` for stories, `1:1` for profile pictures). * You want 4K output resolution for higher detail. When you omit `aspect_ratio`, the model's default applies: models that support `auto` match the input image's aspect ratio, and the rest (currently `qwen-image-2`, `flux-2`, and `flux-2-pro`) default to `4:3`. Passing `"auto"` explicitly to a model that doesn't support it returns a 400 — pick a concrete ratio for those models so the requested `resolution` can be honored. ```json theme={null} { "prompt": "Convert this group photo into a cinematic wide shot with dramatic lighting", "images": ["https://example.com/group.jpg"], "profile_ids": ["abc123", "def456"], "aspect_ratio": "16:9", "resolution": "4K" } ``` *** ## Generate multiple variations Output is non-deterministic -- the same prompt can produce noticeably different results each time. Set `num_output_images` to 2-4 and let the end-user pick the best one. ```json theme={null} { "prompt": "Professional headshot of [[abc123]] in a modern office", "num_output_images": 4 } ``` This is especially useful for generation, where there is no input image anchoring the composition. Multiple variations give a much better chance of landing on a great result in a single API call. *** ## Choose output format and response mode Every edit, generate, and enhance request accepts `output_format` and `response_mode` parameters. **`output_format`** controls the image encoding: * `"jpg"` (recommended) — significantly smaller than PNG, reducing response size and latency. Best for most use cases. * `"png"` — lossless quality. Use when you need transparency support or when re-encoding artifacts would accumulate. **`response_mode`** controls how images are delivered: * `"bytes"` (default) — base64-encoded image data in the `images` field. No data is stored server-side. * `"urls"` — signed CDN download URLs (24-hour expiry) in the `download_urls` field. Use for mobile clients, bandwidth-constrained environments, or when you only need a URL to display or store. Each mode populates its respective field and leaves the other empty — the response never contains both. ```json theme={null} { "prompt": "Professional headshot of [[abc123]]", "output_format": "jpg", "response_mode": "urls" } ``` 4K PNG images in `response_mode: "bytes"` exceed the maximum response size and will be rejected with a 400 error. To use 4K resolution, switch to `response_mode: "urls"`, use `output_format: "jpg"`, or lower the resolution. *** ## Debug identity issues with `known_subjects` Every edit, generate, and enhance response includes a `known_subjects` field that reports which profiles our system successfully recognized and rendered: ```json theme={null} { "images": ["/9j/4AAQ..."], "download_urls": [], "known_subjects": { "counts": { "abc123": 1, "def456": 1 } } } ``` The `counts` dict maps each `profile_id` to the number of times that subject appeared across all output images. **If a subject looks wrong in the output, check `known_subjects` first.** If their `profile_id` is missing from `counts`, our system could not figure out how to preserve that subject's identity. This usually means: * The profile was not included in `profile_ids` (for edit/enhance) or not referenced with `[[...]]` in the prompt (for generate). * The subject in the input photo was difficult to recognize, e.g., due to occlusion, low resolution, or poor lighting. * Too many profiles were passed in `profile_ids`, making it harder for our system to match the right one. * The profile is still training (`IN_PROGRESS`) or failed (`ERROR`). When the `profile_id` *is* present in `counts` but the result still looks off, the issue is more likely prompt-related. Try being more specific about the subject's placement, or generate additional variations. *** ## Next steps Walk through the full workflow from authentication to your first edit. Deep-dive into profile creation, image requirements, and the \[\[profile\_id]] syntax. # Profiles Source: https://docs.photalabs.com/guides/profiles Understand how profiles enable identity-preserving photo editing and generation Profiles are the core concept that makes Phota different from generic image generation. A profile teaches the API what a specific person looks like, so their identity is preserved across all edits and generations. *** ## What is a profile? A profile is a learned representation of a specific person's appearance. You create one by uploading reference photos of the same person — – for the default Full Train tier, or – for Quick Train (see [Training tiers](#training-tiers)). Phota processes these photos to learn the subject's facial features, hair, skin tone, and overall appearance. Once a profile is trained, you can use it across unlimited edit, generate, and enhance requests. The API preserves the person's identity even through dramatic transformations -- placing them on a beach, changing their outfit, or restyling the entire scene. Training photos Upload – reference photos to create a profile (– for Quick Train). Group shots are OK as long as your intended subject is the most common one across all photos. *** ## How profiles work Profiles work in two phases: 1. **Training** -- you upload reference photos and the API learns the subject's appearance (facial features, hair, skin tone, etc.). 2. **Inference** -- when you call `/edit`, `/generate`, or `/enhance`, the API recognizes people in the input image (or described in the prompt) and decides which profiles from the candidate profiles to use to preserve their identities in the output. The fewer candidate profiles you pass, the easier it is for the API to match the right one. If a profile is not provided in the candidate profiles, the API will not consider it when matching. **A typical integration:** your user uploads photos -> you call the profile training endpoint -> you get back a `profile_id` -> you pass that profile in every edit or generation you run for them. Multi-person scenes work too — reference multiple `[[profile_id]]` values in a single prompt. Profiles are opt-in per request. If you don't pass any profile IDs, the API will not attempt to preserve anyone's identity, even if trained profiles exist in your account. For anything where the subject needs to look like a specific person, include the relevant profile IDs. *** ## Profile creation Creating and using a profile follows this flow: ```mermaid actions={false} theme={null} flowchart LR Upload["Upload 10-50 photos (5-10 for Quick Train)"] --> Training["Training starts"] Training --> Poll["Poll status"] Poll -->|VALIDATING / QUEUING / IN_PROGRESS| Wait["Wait 10-30s"] Wait --> Poll Poll -->|READY| Use["Use in /edit, /generate, /enhance"] Poll -->|ERROR| Error["Check error message"] ``` Send the reference image URLs to `POST /profiles/add`. Training starts immediately and runs asynchronously. You receive a `profile_id` right away. ```json theme={null} { "image_urls": ["https://example.com/photo1.jpg", "https://example.com/photo2.jpg", "https://example.com/photo3.jpg"], "tag": "user_abc123" } ``` The `tag` field is optional. Use it to group profiles by your own identifiers (e.g., end-user usernames) so you can filter them later with `GET /profiles/ids?tag=user_abc123`. See [Tagging profiles](#tagging-profiles) below. Call `GET /profiles/{profile_id}/status` until the status is `READY`. Training typically takes \~20 minutes. | Status | Meaning | | - | - | | `VALIDATING` | Images are being validated and prepared. Poll again in 10-30 seconds. | | `QUEUING` | Training job is queued. Poll again in 10-30 seconds. | | `IN_PROGRESS` | Model is actively training. Expected to finish within \~20 minutes. Poll again in 10-30 seconds. | | `READY` | Training completed. The profile is ready for use. | | `ERROR` | Training failed. Check the `message` field for details. | Once ready, reference the profile in any edit, generate, or enhance request. See [Referencing profiles in requests](#referencing-profiles-in-requests) below. *** ## Tagging profiles Tags let you group profiles by your own identifiers -- for example, your end-user's username or account ID. This is useful when your API key manages profiles for many end-users and you need to list only the profiles belonging to a specific one. ### Setting a tag Pass the optional `tag` field when creating a profile: ```json theme={null} { "image_urls": ["https://example.com/photo1.jpg", "..."], "tag": "user_abc123" } ``` Tags are free-form strings up to 128 characters. A profile can have at most one tag, and multiple profiles can share the same tag. A tag is permanent: once a profile has one it cannot be changed or removed, so choose an identifier that stays stable for the life of the profile. Profiles created before you adopted tags can still be tagged -- see [Backfilling tags](#backfilling-tags-onto-existing-profiles) below. ### Backfilling tags onto existing profiles Profiles created before you adopted tags can be tagged after the fact with `POST /profiles/tags`. Send up to 1,000 assignments per request (each profile at most once per batch): ```json theme={null} { "tags": [ { "profile_id": "abc123...", "tag": "user_abc123" }, { "profile_id": "def456...", "tag": "user_def456" } ] } ``` A tag is set only where the profile has none yet -- an existing tag is never overwritten. The `200` response reports every outcome: `updated` counts the profiles tagged by this request, and the `already_tagged` (same value resubmitted), `conflicts` (profile already carries a different tag, echoed as `existing_tag`), and `not_found` (unknown, deleted, or not owned by your account) lists name the profiles that were left unchanged. Resubmitting the same mapping is safe, so you can re-run a backfill after a partial failure. To find profiles that still need tagging, list your profiles with `GET /profiles/ids` and collect entries whose `tag` is `null`. ### Filtering by tag Use the `tag` query parameter on `GET /profiles/ids` to retrieve only profiles with a specific tag: ```bash theme={null} curl https://api.photalabs.com/v2/phota/profiles/ids?tag=user_abc123 \ -H "X-API-Key: YOUR_API_KEY" ``` Omitting the `tag` parameter returns all profiles. *** ## Referencing profiles in requests There are two mechanisms for referencing a profile: the `profile_ids` request field and inline `[[profile_id]]` prompt syntax. **In most cases you pick one, not both.** If you do use both, the API merges them automatically — there's no benefit to duplicating the same ID in both places. Your developer account holds all profiles, but only pass the ones belonging to the end-user whose photo is being processed. This ensures correct identity matching and billing. ### The `profile_ids` field (edit and enhance) This array tells the API which profiles are available as candidates for the request — typically all profiles belonging to a single end-user. The API matches the people in the input photo to these candidates automatically, so you don't have to name anyone in the prompt: ```json theme={null} { "prompt": "Make the sun shine through the trees", "images": ["https://example.com/photo.jpg"], "profile_ids": ["abc123", "def456"] } ``` ### The `[[profile_id]]` prompt syntax (edit and generate) Inline `[[profile_id]]` in the `prompt` attaches a profile to a specific phrase, so the model knows exactly which person the prompt is talking about. This works with any number of subjects — a single reference is perfectly fine: ```json theme={null} { "prompt": "Make [[abc123]] smile wider", "images": ["https://example.com/photo.jpg"] } ``` It's also the only way to reference profiles in generation (which has no `profile_ids` field) and the clearest way to refer to multiple people in one prompt: ```json theme={null} { "prompt": "A photo of [[abc123]] and [[def456]] at a dinner party" } ``` ```json theme={null} { "prompt": "Change [[abc123]]'s clothes to red, and add [[def456]] standing next to them", "images": ["https://example.com/photo.jpg"] } ``` When `[[profile_id]]` appears in an edit prompt, it's automatically merged into the `profile_ids` candidate list — you don't need to list the same ID in both places. Every profile referenced in one request — via `profile_ids`, `[[profile_id]]`, or both — must share the same [model version](#model-versions). Mixing `v1` and `v2` profiles returns HTTP 400 with the error code `MIXED_PROFILE_MODEL_VERSIONS`. ### Which should I use? | Endpoint | Prompt doesn't name the subject | Prompt refers to a specific subject | | - | - | - | | `/edit` | `profile_ids` | `[[profile_id]]` in the prompt (`profile_ids` optional) | | `/generate` | N/A — use `[[profile_id]]` | `[[profile_id]]` in the prompt (no `profile_ids` field exists) | | `/enhance` | `profile_ids` | N/A — `/enhance` has no prompt | Either mechanism is enough on its own for `/edit`. If you use both, the API merges them, so there's no need to list the same ID in both places. *** ## Training tiers Profile training has two tiers, selected with the `training_tier` field on `POST /profiles/add`: | Tier | `training_tier` | Images | Cost | When to use | | - | - | - | - | - | | Full Train (default) | `standard` | – | \$2.90 | Highest fidelity. Recommended for most production use. | | Quick Train | `fast` | – | \$1.49 | Faster, cheaper profile from fewer photos. | Quick Train completes in **\~3 minutes**; Full Train completes in **\~8 minutes** (both excluding queue time). For the strongest profile, use Full Train with images. ## Model versions Profiles are trained on one of two personalization model versions, selected with the `model_version` field on `POST /profiles/add`: | Version | `model_version` | Notes | | - | - | - | | v2 (default) | `v2` | The current personalization model. Used when the field is omitted. | | v1 | `v1` | The original personalization model. Pick this to keep new profiles compatible with existing v1 profiles. | A single account can hold profiles of both versions. Each `/edit`, `/generate`, `/enhance`, or `/remix` request must reference profiles of one version only — a request that mixes `v1` and `v2` profiles returns HTTP 400 with the error code `MIXED_PROFILE_MODEL_VERSIONS` and lists which profiles belong to each version. If you are migrating an end-user from v1 to v2, create new `v2` profiles for all of them before combining them in a request. ## Image requirements for training * **Full Train** (`training_tier: "standard"`): – images * **Quick Train** (`training_tier: "fast"`): – images * **Format**: publicly accessible URLs (signed URLs work) ### Tips for best results
  Clear, well-lit faces\   High-res 1K+ images\   Expressions: neutral, smiling, frowning, surprised\   Angles: front, slight left/right, a profile\   Lighting: indoor, outdoor, sunny, etc.\   Group shots are OK if added person's face is clear
#### Good photos ✅ Well lit and sharp ✅ Diverse expressions and angles ✅ Diverse poses ✅ Diverse accessories (face visible) #### Bad photos ❌ Extreme poses ❌ Small, dark or noisy ❌ Largely covered and occluded ❌ Blurry from movement *** ## Next steps Walk through the full workflow end to end. # Remix Source: https://docs.photalabs.com/guides/remix Restyle a photo to match a reference image — content from one photo, style from another, identity preserved ## One photo's content, another photo's style. The `/remix` endpoint takes two images: an **input image** (the content) and a **reference image** (the style). The output keeps the people that are in your photo, as well as other aspects that you specify in the optional prompt, but renders them in the look of the reference: its lighting, palette, mood, and artistic treatment. No prompt engineering is needed. If you can find a photo with the look you want, you can apply that look to your own.
Input photo Input
+
Style reference Reference
=
Remixed output Output
No prompt (automatic style transfer)
Remixed output steered by a prompt Output
Prompt: "no sunglasses; let her wear an argentina jersey instead, same pose as in reference"
The input and reference stay fixed; flip between the two outputs to see the same remix **without a prompt** and **steered by an optional `prompt`** — here removing the sunglasses and swapping in an Argentina jersey while holding the pose. **When to use it**: applying a brand's visual style to user photos, themed photo features (film looks, editorial styles, seasonal treatments), matching new shots to an existing campaign's aesthetic — anything where the target look already exists as an image. **How profiles help**: Pass [profile](/guides/profiles) IDs in `profile_ids` so that the identity of everyone in your photo stays consistent. *** ## Request | Field | Required | Description | | - | - | - | | `input_image` | yes | The content photo — base64 string or publicly accessible URL | | `reference_image` | yes | The style source — base64 string or publicly accessible URL | | `profile_ids` | no | Profiles to preserve identity for recognized people | | `prompt` | no | Text instructions to steer the remix (e.g. what to keep or emphasize) | | `num_output_images` | no | Number of variations to produce (1–4). Default `1` | | `output_format` | no | `"jpg"` or `"png"`. Default `"png"` | | `response_mode` | no | `"urls"` for signed download URLs, `"bytes"` for inline base64. Default `"bytes"` | Output resolution is fixed at 2K. `base_model`, `aspect_ratio`, and `resolution` are not accepted. Remix is pinned to Nano Banana 2, and the output follows the input image's aspect ratio. ```bash curl theme={null} curl -X POST https://api.photalabs.com/v2/phota/remix \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "input_image": "https://example.com/photo.jpg", "reference_image": "https://example.com/reference.jpg", "profile_ids": ["abc123"], "num_output_images": 2, "output_format": "jpg", "response_mode": "urls" }' ``` ```python Python theme={null} import requests resp = requests.post( "https://api.photalabs.com/v2/phota/remix", headers={ "X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json", }, json={ "input_image": "https://example.com/photo.jpg", "reference_image": "https://example.com/reference.jpg", "profile_ids": ["abc123"], "num_output_images": 2, "output_format": "jpg", "response_mode": "urls", }, ) print(resp.json()) ``` The response has the same shape as `/edit`: an `images` list (URLs or base64, per `response_mode`) plus `known_subjects` counts for any recognized profiles. See the [API reference](/api-reference/introduction) for the full schema. Use `prompt` to protect parts of the input from the restyle — e.g. `"Keep the original background"` — or to emphasize an aspect of the reference's look. *** ## Pricing Remix is billed like [Enhance](/pricing): **\$0.12 per output image**, or **\$0.15** when a trained profile is recognized in the output. *** ## Next steps Create and manage profiles for identity preservation. Rates for every operation, including remix. Visual examples across edit, generate, enhance, and remix. Walk through the full workflow with code examples. # Use cases Source: https://docs.photalabs.com/guides/use-cases Explore what you can build with the Phota API -- editing, generation, enhancement, and remixing with identity preservation The Phota API provides four core capabilities. Each one can optionally leverage trained [profiles](/guides/profiles) to preserve the identity of specific people in the output. *** ## Edit -- transform existing photos The `/edit` endpoint takes one or more input images and a text prompt, then produces a transformed version. When you reference a profile, the person's identity is preserved even through dramatic transformations.
Input photo Before
Edited output After
"Make \[\[abc123]] look at the camera and create a portrait shot."
Input photo Before
Edited output After
"Combine \[\[abc123]] from the first image and \[\[def456]] from the second image in one group shot."
Input photo Before
Edited output After
"Convert to a solo portrait of \[\[abc123]]."
**When to use it**: adding or removing people from group shots, adjusting camera angle or lighting, restyling scenes and backgrounds, creating seasonal or themed variations - anything where the edit needs to look like the real person. **How profiles help**: pass the user's profile IDs in `profile_ids` so the API knows which identities to preserve. The API matches the people in the input photo to the provided profiles automatically. You can also use `[[profile_id]]` in the prompt to reference a specific person explicitly, e.g. `"Make [[abc123]] smile wider"`. **Example request body**: ```json theme={null} { "prompt": "Make [[abc123]] smile wider", "images": ["https://example.com/photo.jpg"], "profile_ids": ["abc123"], "base_model": "nb2", "aspect_ratio": "1:1", "resolution": "4K", "num_output_images": 2, "output_format": "jpg" } ``` You can control the output aspect ratio (e.g. `"9:16"`) and resolution (up to `"4K"`) on every request. See [Best practices](/guides/best-practices#use-aspect-ratio-and-resolution-controls). Switch the underlying image model per request with `base_model`. See [Base model selection](/guides/base-models) for the supported values. *** ## Generate -- create new images from scratch The `/generate` endpoint creates images from a text prompt alone -- no input image is needed. When you reference a profile, the generated person looks exactly like the actual person (or pet).
Generated image Phota
"Artistic Noir Studio Headshot of \[\[abc123]], chest up. Introspective expression, gaze directed slightly off-camera, subtle three-quarter head tilt."
Generated image Phota
"Playful, photorealistic portrait of \[\[abc123]] and \[\[def456]], emerging through torn festive paper. Their hands emerge through the torn paper, holding the edges as if the subjects are emerging from the paper."
Generated image Phota
"Professional corporate headshot of \[\[abc123]]. Off-center to the right, smiling at camera, confident expression."
**When to use it**: professional headshots, holiday cards and family portraits, profile pictures in any setting or style, couple and group photos where everyone doesn't need to be in the same room — any scenario where you need a photo-realistic image of specific people. **How profiles help**: the generated person matches the trained profile's appearance -- face shape, features, hair, and skin tone are all preserved. Reference profiles directly in the prompt with `[[profile_id]]` syntax. **Example request body**: ```json theme={null} { "prompt": "A professional headshot of [[abc123]] in a modern office", "base_model": "nb2", "num_output_images": 4, "output_format": "jpg" } ``` Generation is non-deterministic — set `num_output_images` to 2–4 and let the user pick the best result. The `/generate` endpoint does not accept `images` or `profile_ids`. Reference profiles in the prompt with `[[profile_id]]` syntax instead. If you need to transform an existing photo, use `/edit`. *** ## Enhance -- automatic quality improvement The `/enhance` endpoint improves a photo automatically -- no text prompt required. Unlike most AI image services, Phota infers all enhancement parameters on its own: lighting, sharpness, color balance, and overall quality. An optional `prompt` lets you guide the enhancement when you want to.
Original photo Before
Enhanced photo After
Original photo Before
Enhanced photo After
Original photo Before
Enhanced photo After
**When to use it**: photos taken in poor lighting, blurry or noisy images, photos with compression artifacts — any photo where quality fell short. **How profiles help**: when profile IDs are provided, enhancement preserves and can improve the rendering of known faces in the photo. **Example request body**: ```json theme={null} { "image": "https://example.com/photo.jpg", "profile_ids": ["abc123"], "num_output_images": 2, "output_format": "jpg" } ``` *** ## Remix -- restyle with a reference image The `/remix` endpoint takes an input image (the content) and a reference image (the style), and produces your photo rendered in the reference's look — lighting, palette, mood, and artistic treatment. No prompt needed, though an optional `prompt` can steer the result (second slide).
Input photo Input
+
Style reference Reference
=
Remixed output Output
No prompt — pure style transfer
Remixed output steered by a prompt Output
Prompt: "no sunglasses; let her wear an argentina jersey instead, same pose as in reference"
**When to use it**: applying a brand's visual style to user photos, themed photo features (film looks, editorial styles, seasonal treatments), matching new shots to an existing campaign's aesthetic. **How profiles help**: pass profile IDs in `profile_ids` and recognized people keep their real likeness through the restyle. **Example request body**: ```json theme={null} { "input_image": "https://example.com/photo.jpg", "reference_image": "https://example.com/reference.jpg", "profile_ids": ["abc123"], "num_output_images": 2, "output_format": "jpg", "response_mode": "urls" } ``` See the [Remix guide](/guides/remix) for the full walkthrough. *** ## Next steps Learn how to create and manage profiles for identity preservation. Walk through the full workflow with code examples. Explore the full endpoint documentation. # Phota API Source: https://docs.photalabs.com/index GenAI-powered photo editing, generation, enhancement, and remixing with identity preservation ## Welcome to the Phota API Phota is the API for GenAI-powered photography, specialized to solve the last-mile problem of identity preservation. Generic image models can't reliably preserve what a specific person looks like. Phota can. By running our proprietary identity-preserving model together with flagship image editing models (such as Nano Banana), Phota API allows you to integrate identity-preserved photo editing, generation, and enhancement into your application. Train a **profile** from as few as reference photos of a subject (up to for highest fidelity). Then edit, generate, or enhance images where that person is accurately represented — every time. Reference one or more profiles in any request using `[[profile_id]]` syntax. **Base URL**: `https://api.photalabs.com/v2/phota` *** ## Capabilities ### Edit Provide one or more input images and a text prompt describing the desired transformation. Phota applies the edit while preserving the identity of any known subjects in the photo.
Input photo Before
Edited output After
"Make \[\[abc123]] look at the camera and create a portrait shot."
Input photo Before
Edited output After
"Combine \[\[abc123]] from the first image and \[\[def456]] from the second image in one group shot."
Input photo Before
Edited output After
"Convert to a solo portrait of \[\[abc123]]."
Fix expressions, add or remove people from group shots, adjust camera angle and lighting, clean up artifacts — all while keeping subjects with known profiles accurately represented. ### Generate Describe what you want in a text prompt. Reference one or more trained profiles so every generated image looks like the actual person (or pet).
Generated image Phota
"Artistic Noir Studio Headshot of \[\[abc123]], chest up. Introspective expression, gaze directed slightly off-camera, subtle three-quarter head tilt."
Generated image Phota
"Playful, photorealistic portrait of \[\[abc123]] and \[\[def456]], emerging through torn festive paper. Their hands emerge through the torn paper, holding the edges as if the subjects are emerging from the paper."
Generated image Phota
"Professional corporate headshot of \[\[abc123]]. Off-center to the right, smiling at camera, confident expression."
Great for professional headshots, holiday cards, family portraits, profile pictures, influencer-style shoots in any setting. ### Enhance Upload a photo, get a better photo back. **No prompt required.** Phota automatically improves lighting, composition, color, and overall photographic quality — or pass an optional `prompt` to steer the enhancement.
Original photo Before
Enhanced photo After
Original photo Before
Enhanced photo After
Original photo Before
Enhanced photo After
Works on photos with poor lighting, compression artifacts, blur, and other common defects. ### Remix Provide your photo and a reference image with the look you want. Phota renders your photo's content in the reference's style — lighting, palette, mood — while keeping known subjects accurately represented.
Input photo Input
+
Style reference Reference
=
Remixed output Output
No prompt — pure style transfer
Remixed output steered by a prompt Output
Prompt: "no sunglasses; let her wear an argentina jersey instead, same pose as in reference"
Apply a brand look, a film aesthetic, or any visual treatment that already exists as an image — no prompt engineering required. See the [Remix guide](/guides/remix). *** ## How profiles work To preserve someone's identity, you first create a **profile** by uploading reference photos of the same subject — from as few as (Quick Train) up to (Full Train). Phota learns their appearance and returns a `profile_id` you can reference in any request using `[[profile_id]]` syntax. People and pets (cats and dogs) are supported. Group photos are fine — Phota automatically creates a profile for the subject that appears most often across all uploaded images. The API only considers profiles explicitly passed in each request -- if you don't include any, no identity preservation is applied, even if trained profiles exist in your account. **A typical integration looks like this:** your user uploads photos of themselves, you train a profile via the API, and then pass that profile in every edit or generation you run for them. Multi-person scenes work too — just reference multiple `[[profile_id]]` values in a single prompt. Understand the full profile creation process, prompt syntax, and image requirements. *** ## Get started Learn how to authenticate your API requests with an API key. See detailed examples of Edit, Generate, and Enhance with code snippets. Explore every endpoint, request schema, and response format. Deep-dive into profiles, identity preservation, and the \[\[profile\_id]] syntax. See what's new and what's changed. # Pricing Source: https://docs.photalabs.com/pricing Per-image and per-profile rates Per-image cost is shown as base / with-identity. With-identity applies when a trained profile is recognized in the output. | Model | 1K | 2K | 4K | | - | - | - | - | | Nano Banana 2 (nb2, default) | \$0.08 / \$0.10 | \$0.12 / \$0.15 | \$0.16 / \$0.20 | | Nano Banana 2.1 (nb2.1) | \$0.05 / \$0.07 | \$0.07 / \$0.10 | \$0.14 / \$0.18 | | Qwen Image 2 (qwen-image-2) | \$0.04 / \$0.05 | \$0.04 / \$0.05 | — | | Flux 2 Dev (flux-2) | \$0.02 / \$0.03 | \$0.06 / \$0.08 | — | | Seedream 5.0 Pro (seedream-5-pro) | \$0.07 / \$0.09 | \$0.14 / \$0.17 | — | Prices are per output image in USD. **GPT Image 2, GPT Image 2.5 Sunburst, and GPT Image 2.5 Flare** — token-metered (varies per request), one rate sheet for all three | Component | Rate | | - | - | | Input text tokens | \$5.00 per 1M | | Input image tokens | \$8.00 per 1M | | Output image tokens | \$30.00 per 1M | | Identity surcharge | +20% | Higher quality tiers and larger images consume more output tokens; every quality tier uses the same rates. Custom sizes reserve 70¢ per output image up front — \$1.00 above 3,686,400 pixels — and settle to actual usage, refunding the difference. **Flux 2 Pro** — megapixel-metered (varies per request) | Component | Rate | | - | - | | First megapixel | \$0.03 | | Each additional megapixel | \$0.015 | | Identity surcharge | +20% | A request's megapixels are the output image plus every reference image you send, rounded up to the next whole megapixel. Each reference image counts up to 4 megapixels, however large you upload it. | Example — one reference image | Base | With identity | | - | - | - | | 1K edit | \$0.09 | \$0.11 | | 2K edit | \$0.14 | \$0.17 | | Operation | Cost | Unit | | - | - | - | | Enhance | \$0.12 / \$0.15 | per output image | | Remix | \$0.12 / \$0.15 | per output image | | Profile training — Full Train | \$2.90 | per profile | | Profile training — Quick Train | \$1.49 | per profile | Profile training has two tiers. **Full Train** uses – images and produces the highest-fidelity profile. **Quick Train** uses – images for a faster, lower-cost profile. Pick the tier with the `training_tier` field on `POST /profiles/add`; it defaults to Full Train. See [Profiles](/guides/profiles) for guidance on choosing. View your credit balance, add credit, manage auto top-up, and download invoices. Pick which image model powers your `/edit` and `/generate` requests. # Release notes Source: https://docs.photalabs.com/release-notes What's new and what's changed in the Phota API ## v0.12.0 -- New base model: Nano Banana 2.1 * **New `base_model` option `nb2.1`.** Google's Nano Banana 2.1 takes the same request contract as `nb2`: `resolution` `"1K"`, `"2K"`, and `"4K"`, auto aspect ratio, and, for now, the same flat per-image price. Its pricing is subject to change. See [Base model selection](/guides/base-models) and [Pricing](/pricing). * **Upcoming default change.** `nb2` stays the default for now. We plan to make `nb2.1` the default in a future release; pass `base_model: "nb2"` explicitly to keep Nano Banana 2. *** ## v0.11.0 -- GPT Image custom sizes and expanded quality * **Exact GPT Image output sizes.** `/edit` and `/generate` accept `custom_resolution`, such as `"1536x864"`, on GPT Image 2 and both GPT Image 2.5 models. Custom sizes are capped at 3,686,400 pixels. * **Higher GPT Image 2.5 quality.** Sunburst and Flare accept `quality: "xhigh"` and `"max"`. See [Base model selection](/guides/base-models) for size constraints and [Pricing](/pricing) for rates. *** ## v0.10.0 -- New base models: GPT Image 2.5 Sunburst and GPT Image 2.5 Flare ### New features * **Two new `base_model` options.** `gpt-image-2.5-sunburst` and `gpt-image-2.5-flare` join the lineup with the same contract as `gpt-image-2`: `resolution` `"1K"`, `"2K"`, and `"4K"`, the optional `quality` tier, and token-metered billing at the same rates. See [Base model selection](/guides/base-models) and [Pricing](/pricing). *** ## v0.9.3 -- `/v2` base URL ### Changes * **New base URL `https://api.photalabs.com/v2/phota`.** Every endpoint is available under `/v2/phota` with the same request and response contracts as `/v1/phota`. `/v1/phota` keeps working unchanged — no migration is required. Async jobs report a `poll_url` under whichever base URL they were submitted to. *** ## v0.9.2 -- Profile model versions ### New features * **`model_version` parameter** on `POST /profiles/add`. Optional, accepts `"v1"` (the original personalization model) or `"v2"` (the current one, and the default). An account can hold profiles of both versions. See [Model versions](/guides/profiles#model-versions). ### Changes * **One model version per request.** `/edit`, `/generate`, `/enhance`, and `/remix` now return HTTP 400 with the new error code `MIXED_PROFILE_MODEL_VERSIONS` when the referenced profiles (from `profile_ids` and `[[profile_id]]` combined) span both `v1` and `v2`. The error detail lists which profiles belong to each version. *** ## v0.9.1 -- Tag backfill for existing profiles ### New features * **`POST /v1/phota/profiles/tags`** backfills [tags](/guides/profiles#tagging-profiles) onto profiles created before you adopted tagging. Submit up to 1,000 `{profile_id, tag}` assignments per request; each tag is set only where the profile has none yet (existing tags are never overwritten), and the response reports every outcome (`updated`, `already_tagged`, `conflicts`, `not_found`), so resubmitting a mapping is always safe. *** ## v0.9.0 -- New base models: Flux 2 Pro and Seedream 5.0 Pro ### New features * **Two new `base_model` options.** `flux-2-pro` and `seedream-5-pro` join the existing lineup, both supporting `resolution` `"1K"` and `"2K"` (4K returns HTTP 400, like `flux-2` and `qwen-image-2`). `seedream-5-pro` is billed flat per image; `flux-2-pro` is megapixel-metered, billed per processed megapixel the same way `gpt-image-2` is billed per token. See [Base model selection](/guides/base-models) and [Pricing](/pricing). *** ## v0.8.0 -- Reve base model removed ### Behavior changes * **`base_model: "reve"` is no longer accepted.** Reve is sunsetting its public API on August 14, 2026, so the model can no longer be offered. Requests to `/edit` or `/generate` that pass `base_model: "reve"` now return HTTP `422` (unknown base model id), and `reve` no longer appears in the OpenAPI `base_model` enum or on [Pricing](/pricing). All other base models are unaffected; requests that omit `base_model` continue to run on Nano Banana 2. *** ## v0.7.1 -- Typed error for imageless completions ### Behavior changes * **New `NO_IMAGE_GENERATED` error code.** When the model completes without producing an image (a soft refusal or a transient glitch — not a content-moderation block), the API now returns HTTP `422` with error code `NO_IMAGE_GENERATED` instead of a generic `500 INTERNAL_ERROR`. The request is not charged, and retrying may succeed. Asynchronous jobs that fail this way report the same code in `error.code`. *** ## v0.7.0 -- Asynchronous requests ### New features * **Asynchronous mode** on `/edit`, `/generate`, `/enhance`, and `/remix`. Pass `async: true` (with `response_mode: "urls"`) and the call returns `202` with a `job_id` instead of blocking; poll `GET /v1/phota/jobs/{job_id}` for the result. Your client submits the job and polls (or receives a callback) when it's ready — no long-lived connection to hold open, and less overhead for your integration. See [Asynchronous requests](/api/async-requests). * **Job callbacks** — pass an optional `callback_url` to be notified when a job finishes. Callback bodies are HMAC-signed (`X-Phota-Signature`); fetch your signing secret from `GET /v1/phota/webhook-secret`. Polling remains authoritative. * **Idempotent submits** — pass an optional `client_request_id`; a retried submit with the same value returns the original job instead of creating (or charging for) a duplicate. ### Behavior changes * **Graceful handling of long synchronous requests.** A synchronous request that runs past its time limit now returns a clean `504` and automatically refunds the reserved cost, instead of dropping the connection. For long-running edits, prefer `async: true` — it runs the work in the background with no connection to hold open. *** ## v0.6.0 -- Remix is generally available ### New features * **`POST /remix`** — restyle an image to match a reference image. Provide an `input_image` (the content) and a `reference_image` (the style); the output keeps the input's content rendered in the reference's look. Optional `profile_ids` preserve recognized identities through the restyle, and an optional `prompt` steers the result. Output resolution is fixed at 2K. Billed like Enhance: \$0.12 per output image, \$0.15 with identity. See the [Remix guide](/guides/remix). *** ## v0.5.0 -- Quick Train tier for profiles ### New features * **`training_tier` parameter** on `POST /profiles/add`. Optional, accepts `"standard"` (default) or `"fast"`. * `"standard"` (Full Train) — – images, \$2.90 per profile, highest fidelity. * `"fast"` (Quick Train) — – images, \$1.49 per profile, faster turnaround at lower cost. * Image count is validated per tier; out-of-range counts return HTTP 422. ### Behavior changes * **Full Train (`standard`) minimum image count lowered from 30 to .** Existing requests sending 30–50 images are unaffected. * **Quick Train completes in \~3 minutes, Full Train in \~8 minutes** (both excluding queue time). **Full Train length now scales with image count** (previously fixed at the -image equivalent) — requests sending 30– images will see slightly shorter, proportional training. For the strongest profile, send images. *** ## v0.4.0 -- Quality selection on GPT Image 2 ### New features * **`quality` parameter** on `/edit` and `/generate`. Optional, accepts `"auto"`, `"low"`, `"medium"`, or `"high"`. Only supported when `base_model` is `gpt-image-2`; sending `quality` for any other model returns HTTP 400. Settlement remains token-driven, so partners pay for the actual output tokens regardless of tier. ### Behavior changes * **Default quality on `gpt-image-2` is now `"auto"`** — Requests that omit `quality` previously landed on the `"high"` tier; they now land on OpenAI's auto tier. Pass `"quality": "high"` explicitly if you want the previous behavior. *** ## v0.3.0 -- Base model selection ### New features * **`base_model` parameter** on `/edit` and `/generate`. Pick between `nb2` (Nano Banana 2, default), `gpt-image-2` (GPT Image 2), `qwen-image-2`, `flux-2`, and `reve`. Phota's identity layer composes on top of any of them. Unknown ids return HTTP 422; unsupported resolutions (e.g. 4K on `flux-2` or `qwen-image-2`) return HTTP 400. See [Base model selection](/guides/base-models). * **Per-model pricing** — Each base model has its own per-image rate plus a with-identity rate that applies when a trained profile is recognized in the output. GPT Image 2 is billed per token. See [Pricing](/pricing). ### Improvements * **Less wardrobe and background leakage on `/generate`** — Clothing, accessories, and background from the profile's training photos are less likely to bleed into `/generate` outputs. Prompts that specify wardrobe or setting are followed more reliably. *** ## v0.2.1 ### New features * **Enhance prompts** — The `/enhance` endpoint now accepts an optional `prompt` field with text instructions to guide the enhancement. When omitted, enhancement parameters continue to be inferred automatically. *** ## v0.2.0 ### New features * **Output format selection** — New `output_format` parameter on edit, generate, and enhance endpoints. Choose between `"png"` (lossless) and `"jpg"` (smaller files). Default: `"png"`. * **Response mode** — New `response_mode` parameter (`"bytes"` or `"urls"`) on edit, generate, and enhance endpoints. Default: `"bytes"` (base64-encoded images). Set to `"urls"` to receive signed CDN download URLs (24-hour expiry) instead. Each mode populates its respective response field; the other is returned empty. ### Deprecation notices * **Default format change** — The default `output_format` will change from `"png"` to `"jpg"` on **2026-05-08**. Set `output_format` explicitly in your requests to avoid disruption. Responses include `X-Phota-Deprecation-Date: 2026-05-08` header. *** ## v0.1.1 ### New features * **Profile tags** — Profiles now accept an optional `tag` field on creation, allowing you to namespace profiles by your own identifiers (e.g., end-user IDs). `GET /profiles/ids` accepts a `?tag=` query parameter to filter profiles by tag. ### Improvements * **Image format handling** — Input images exceeding 4096px are automatically resized to 4K. Supported formats (JPEG, PNG, WebP, HEIC/HEIF) are now documented in the OpenAPI spec field descriptions. PNGs are automatically transcoded to optimized JPG. * **Request tracing** — Every response now includes an `X-Request-Id` header, and error responses include a `request_id` field. Use these when reporting issues for faster debugging. * **Request timeout** — Maximum request duration increased from 360s to 600s, fixing 504 timeouts on long-running generation requests. * **Content moderation errors** — Content moderation blocks now return HTTP 400 with error type `CONTENT_MODERATION` instead of HTTP 500. ### Bug fixes * **Training error messages** — When no subject is detected in training images, the API now returns a descriptive HTTP 400 error instead of a generic 500 error. *** ## v0.1.0 -- Initial release This is the first public release of the Phota API. ### Endpoints **Studio** * `POST /edit` -- Edit images using a text prompt and optional profile references. Supports base64 and URL input images. * `POST /generate` -- Generate new images from a text prompt alone, without any input image. Supports profile references for identity preservation. * `POST /enhance` -- Automatically enhance a photo without a text prompt. **Profiles** * `POST /profiles/add` -- Create a new profile from – reference image URLs. Training runs asynchronously. * `GET /profiles/{profile_id}/status` -- Poll the training status of a profile (IN\_PROGRESS, READY, or ERROR). * `GET /profiles/ids` -- List all profile IDs for the authenticated account. * `GET /profiles/{profile_id}/profile_picture` -- Retrieve a profile picture as a JPEG image. * `DELETE /profiles/{profile_id}` -- Permanently delete a profile and all associated data. ### Features * **Identity preservation** -- Train profiles from reference photos and use them across edits and generations. Reference profiles in prompts with the `[[profile_id]]` syntax to control where specific people appear. * **Pro mode** -- Available on edit and generate endpoints. Enables better instruction following and quality at the cost of higher costs. Unlocks aspect ratio and resolution control. * **Multi-image output** -- Generate up to 4 output images per request. * **Flexible input** -- Accept images as raw base64 strings or publicly accessible URLs. * **Aspect ratio and resolution control** -- Choose from 11 aspect ratios and up to 4K resolution when pro mode is enabled. * **Billing metadata** -- Responses include `known_subjects` with counts of known subjects generated per profile, used for billing purposes. ### Authentication * All endpoints require the `X-API-Key` header. See [Authentication](/api/authentication) for details. # Support Source: https://docs.photalabs.com/support Get help and connect with the Phota community Have questions, feedback, or need help? Connect with other developers and the Phota team on Discord. ## Other resources Manage your API keys and account settings. Read about new features, tutorials, and behind-the-scenes updates. Learn more about Phota and our products. Browse the full API endpoint documentation.