EZPhoto API
One REST API for image operations: background removal, upscaling, and AI product shots. You send an image, you get a URL back. Everything else on this page is detail.
Base URL https://api.ezphoto.ai
Format JSON in, JSON out (multipart accepted for uploads)
Auth Authorization: Bearer <key>
Errors RFC 7807 application/problem+json
Authentication
Every request needs an API key in the Authorization header. Keys start with
ez_live_ and are shown exactly once when created — we store only a hash.
Get one from the dashboard; the free plan includes 50 operations per month.
Authorization: Bearer ez_live_7d9fa9f5e084d1c4…
Requests without a valid key return 401. Disabled keys fail the same way, immediately.
Quickstart
Remove the background from an image in one call. The response is the result — no polling for this endpoint.
curl -X POST https://api.ezphoto.ai/v1/remove-background \
-H "Authorization: Bearer $EZPHOTO_KEY" \
-H "Content-Type: application/json" \
-d '{"image_url": "https://your.app/photo.jpg"}'
import requests
r = requests.post(
"https://api.ezphoto.ai/v1/remove-background",
headers={"Authorization": f"Bearer {EZPHOTO_KEY}"},
json={"image_url": "https://your.app/photo.jpg"},
)
print(r.json()["result_url"])
const res = await fetch("https://api.ezphoto.ai/v1/remove-background", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.EZPHOTO_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ image_url: "https://your.app/photo.jpg" }),
});
const { result_url } = await res.json();
Response, typically in about a second:
{
"request_id": "0f04c41c-a886-4da8-b211-3d4b12007110",
"op": "remove-background",
"result_url": "https://…/0f04c41c-….png",
"expires_at": "2026-08-14T22:28:13Z"
}
Image inputs
Every operation accepts the image two ways. Pick one per request:
URL mode (recommended)
Send Content-Type: application/json with an image_url. The URL must be
publicly reachable over http(s) — our inference providers fetch it directly, so localhost and
private-network URLs will fail with a 422.
Direct upload
Send multipart/form-data with the file in a field named image. Other
parameters (factor, scene, prompt, webhook_url) go in ordinary form fields.
| Limit | Value |
|---|---|
| Max upload size | 20 MB (413 beyond that) |
| Formats | PNG, JPEG, WebP, GIF — detected by content sniffing, not filename |
Results & retention
Results are delivered as a result_url — a signed URL on our storage, valid for
24 hours (the exact moment is in expires_at). After that the file is deleted
and the URL returns an error. Copy results to your own storage if you need them longer.
Retention is the product, not a caveat: originals are never stored, and results cannot outlive their TTL. There is no setting to change this.
Sync vs. async
remove-background is synchronous: the HTTP response contains the result.
upscale and product-shot are heavier, so they return
202 Accepted with a job object immediately:
{
"id": "90ac31c0-b2d8-4d2b-aace-66cd072d7429",
"op": "upscale",
"status": "queued",
"result": null,
"error": null,
"created_at": "2026-08-13T21:23:47Z",
"updated_at": "2026-08-13T21:23:47Z"
}
A job moves queued → processing → succeeded | failed. Get the outcome either way:
- Poll
GET /v1/jobs/{id}— most jobs finish in a few seconds. - Webhook — pass
webhook_urland we POST the finished job to you. See Webhooks.
POST /v1/remove-background
Remove the background from an image. Synchronous · $0.01 per image.
Parameters
| Field | Type | Description | |
|---|---|---|---|
image_url | string | required* | Public http(s) URL of the source image. *Or upload the file as multipart image. |
Response 200
| Field | Type | Description |
|---|---|---|
request_id | uuid | Unique id of this operation (also in your usage ledger). |
op | string | remove-background |
result_url | string | Signed URL of the PNG cutout with transparent background. |
expires_at | timestamp | When result_url stops working (24h). |
curl -X POST https://api.ezphoto.ai/v1/remove-background \
-H "Authorization: Bearer $EZPHOTO_KEY" \
-H "Content-Type: application/json" \
-d '{"image_url": "https://your.app/photo.jpg"}'
curl -X POST https://api.ezphoto.ai/v1/remove-background \
-H "Authorization: Bearer $EZPHOTO_KEY" \
-F "image=@product-photo.jpg"
POST /v1/upscale
Upscale an image 2x or 4x with detail enhancement. Async · $0.05 per image.
Parameters
| Field | Type | Description | |
|---|---|---|---|
image_url | string | required* | Public http(s) URL of the source image. *Or multipart image. |
factor | number | 2 (default) or 4. Anything else is a 400. | |
webhook_url | string | http(s) URL to POST the finished job to. |
Response 202 — a job object. On success, result contains:
"result": {
"result_url": "https://…/90ac31c0-….png",
"expires_at": "2026-08-14T21:23:52Z"
}
curl -X POST https://api.ezphoto.ai/v1/upscale \
-H "Authorization: Bearer $EZPHOTO_KEY" \
-H "Content-Type: application/json" \
-d '{"image_url": "https://your.app/photo.jpg", "factor": 4,
"webhook_url": "https://your.app/hooks/ezphoto"}'
POST /v1/product-shot
Place a product photo into a styled scene. Async · $0.10 per image.
Parameters
| Field | Type | Description | |
|---|---|---|---|
image_url | string | required* | Public http(s) URL of the product photo. *Or multipart image. |
scene | string | one of scene/prompt | A preset: studio-white, marble-table, or outdoor-lifestyle. |
prompt | string | Custom scene description; overrides scene when both are sent. | |
webhook_url | string | http(s) URL to POST the finished job to. |
Scene presets
| Preset | Look |
|---|---|
studio-white | Clean white studio background, soft even lighting, subtle shadow. |
marble-table | White marble surface, airy interior, natural window light. |
outdoor-lifestyle | Golden-hour outdoor scene, natural setting. |
Generated images pass a safety check; flagged content fails the job with a 422-class error rather than returning a result.
curl -X POST https://api.ezphoto.ai/v1/product-shot \
-H "Authorization: Bearer $EZPHOTO_KEY" \
-F "image=@flat-lay.jpg" \
-F "scene=marble-table"
GET /v1/jobs/{id}
Fetch the current state of an async job. Jobs are visible only to the key that created them.
curl https://api.ezphoto.ai/v1/jobs/90ac31c0-b2d8-4d2b-aace-66cd072d7429 \
-H "Authorization: Bearer $EZPHOTO_KEY"
status is one of queued, processing, succeeded, failed.
On failed, error holds a stable human-readable message. Unknown or foreign job ids return 404.
Idempotency
Network timeouts happen; paying twice for one image shouldn't. Send an
Idempotency-Key header (any string up to your choosing, e.g. an order id) on any POST:
Idempotency-Key: order-4812-hero
- Retrying the same request replays the original response — same
request_idor job, no second charge — with anIdempotency-Replayed: trueheader. - Reusing a key with a different body is a
422. - Retrying while the original is still running is a
409— back off and retry. - If the original request failed, the key is released and the retry executes normally.
Replays match on the normalized request, so re-uploading the same file works even though multipart encodings differ between attempts.
Webhooks
Pass webhook_url when creating an async job and we POST the finished job object
(identical to GET /v1/jobs/{id}) to that URL when it succeeds or fails.
| Behavior | Value |
|---|---|
| Delivery attempts | 3 — immediately, then +5s, then +25s |
| Per-attempt timeout | 10 seconds |
| Success criterion | Any 2xx response from your endpoint |
Webhooks are best-effort: if all attempts fail, the job result is still available by polling. Design your handler to be idempotent — a slow 2xx can cause a duplicate delivery.
Errors
Every error is application/problem+json (RFC 7807) with a stable shape:
{
"type": "about:blank",
"title": "Bad Request",
"status": 400,
"detail": "factor must be 2 or 4."
}
| Status | When |
|---|---|
400 | Invalid parameters — missing image_url, bad factor, unknown scene, non-image upload. |
401 | Missing, unknown, or disabled API key. |
404 | Job not found (or belongs to another key). |
409 | Idempotency-Key retry while the original request is still in flight. |
413 | Upload over 20 MB. |
415 | Content type is neither JSON nor multipart. |
422 | Upstream rejected the input (unreachable image_url, safety-flagged content), or Idempotency-Key reused with a different body. |
429 | Rate limit or monthly quota exceeded — see limits. |
502 | The upstream operation failed. Safe to retry. |
Rate limits & quotas
| Limit | Default | On exceeding |
|---|---|---|
| Request rate | 5 req/s sustained, bursts to 10, per key | 429 with Retry-After |
| Free plan | 50 operations per calendar month | 429, resets on the 1st |
| Spend cap | Optional per-key monthly ceiling | 429, resets on the 1st |
Queued jobs count against your quota the moment they're accepted, so a burst of submissions can't overshoot the cap. Idempotent replays never count twice.
Pricing
| Operation | Price |
|---|---|
| remove-background | $0.01 |
| upscale | $0.05 |
| product-shot | $0.10 |
You're billed only for successful operations — failed jobs cost nothing.