/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 /v1/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.
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.Submit the job
Add The
async: true and response_mode: "urls" to any image request. The endpoint
returns 202 Accepted with a job handle instead of the image payload:202 response is a job handle:Poll for the result
Poll A succeeded job carries the same result shape as the synchronous response, with
GET /v1/phota/jobs/{job_id} until status is succeeded or failed:download_urls populated (images is always empty in async mode):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).(Optional) Receive a signed callback
Pass a Verify a received callback by recomputing the HMAC over the raw request body
and comparing in constant time:
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=<hex digest>. Fetch your secret once from:Python
Idempotency
Pass aclient_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
Quickstart
The synchronous workflow, end to end.
API reference
Full request and response schemas for every endpoint.
