# 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.
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
+
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.
**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).
**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.
**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
+
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.
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).
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.
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
+
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.