API Documentation
PhotoFig API
Queue product image edits and resizes, run templates over whole batches, follow progress, and receive signed webhook events with API keys.
Need a machine-readable schema? Download the OpenAPI JSON.
On this page
Quickstart
Create an API key in Settings. Replace YOUR_API_KEY below and save a photo as product.jpg.
Submit a photo
Upload the photo to add a product shadow. The response includes a job ID and statusUrl.
curl https://api.photofig.com/v1/tools/ai-shadows/edit \ -H "Authorization: Bearer YOUR_API_KEY" \ -F "image=@product.jpg"Check the job
Replace STATUS_URL with the returned statusUrl. If status is queued or processing, wait before checking again.
curl "STATUS_URL" \ -H "Authorization: Bearer YOUR_API_KEY"Download the result
Once status is completed, replace RESULT_URL with the returned resultUrl to save the image.
curl -fL "RESULT_URL" -o photofig-result.png
Use Webhooks to receive completion events automatically.
Authentication
API routes accept API keys generated from Settings. The full secret is shown only once when the key is created.
Authorization: Bearer pf_live_...API keys work only with the endpoints on this page; every other PhotoFig route refuses them. A key acts for the person who created it, in the workspace it was created in.
Limits and Quotas
API access is included on Pro and Studio. GET /v1/account shows your plan's limits, including the hourly request allowance in limits.rateLimitPerHour.
Every authenticated request counts toward the hourly allowance, job polling included. Responses carry X-RateLimit-Limit and X-RateLimit-Remaining. When the allowance runs out, the API answers 429 rate_limit_exceeded with a Retry-After header.
mask uses background removals, ai-bg and ai-shadows use one AI credit each, and resize uses neither. Credits are charged only when a job completes.
Queued and running jobs count toward your plan's in-flight limit. Past it, new edits get 429 too_many_jobs until earlier jobs finish.
POST /v1/uploads and POST /v1/tools/{tool_id}/edit accept an optional X-Idempotency-Key header. Retrying with the same key returns the first response instead of starting a second job, so use a new key for every distinct request.
Account
/v1/accountUse this route as a credential smoke test and to inspect plan limits, remaining credits, and available tools.
{
"accountId": "uuid",
"workspaceId": "uuid",
"plan": "pro",
"apiEnabled": true,
"credits": {
"remaining": 2000,
"bonusRemaining": 0,
"periodEndsAt": "2026-11-01T00:00:00Z"
},
"limits": {
"maxFileSizeBytes": 10485760,
"rateLimitPerHour": 60,
"concurrentJobs": 3,
"maxApiKeys": 1,
"freeBgRemovals": 0,
"bgRemovalsUsed": 0,
"aiCredits": 2000
},
"tools": [
"mask",
"ai-bg",
"ai-shadows",
"resize"
]
}Tools
/v1/tools/v1/tools/{tool_id}Each tool accepts a normal product image through imageUrl, uploadSessionId, or a multipart upload. You do not need to call mask first: ai-bg and ai-shadows remove the background themselves.
mask
Removes the background and returns a PNG subject cutout with transparency. Use it when you need the product isolated for your own compositing or marketplace workflow.
ai-shadows
Adds a realistic product shadow and returns an image ready to place on a clean background. Works best on product photos where the subject is clearly visible and not hidden by a busy scene.
ai-bg
Generates a new product background from a short theme, such as warm oak tabletop or minimal white studio. Submit the product image directly; a separate background-removal call is not required.

resize
Fits a photo to an exact canvas size and keeps its own background, like Catalog Resize in the app. Choose fit or fill, where the photo sits, and what fills the space. Uses no AI credits.
Upload and Edit in One Call
/v1/tools/{tool_id}/editThis is the simplest path: submit an edit with an imageUrl when the source image is already reachable by URL, or send a multipart image field for a quick local-file request. It is best when you only need to run one tool against one input.
Choose this when you have one image, one edit, and no need to reuse the uploaded source.
curl -X POST https://api.photofig.com/v1/tools/ai-shadows/edit \
-H "Authorization: Bearer pf_live_..." \
-F "image=@product.jpg" \
-F "seed=128742"Upload First, Edit Later
/v1/uploadsCreate an upload session to send the image bytes straight to storage, then start an edit with the returned uploadSessionId. The size limit is the same as for a multipart request, and each session is used by one edit.
Choose this when your client should not stream the image through the edit request, for example a browser uploading directly to storage.
UPLOAD_JSON=$(curl -s -X POST https://api.photofig.com/v1/uploads \
-H "Authorization: Bearer pf_live_..." \
-H "Content-Type: application/json" \
-d '{
"contentType": "image/jpeg",
"filename": "product.jpg",
"sizeBytes": 1842031
}')
curl -X PUT "$(echo "$UPLOAD_JSON" | jq -r '.uploadUrl')" \
-H "Content-Type: image/jpeg" \
--data-binary @product.jpg
curl -X POST https://api.photofig.com/v1/tools/ai-shadows/edit \
-H "Authorization: Bearer pf_live_..." \
-H "Content-Type: application/json" \
-d "{
\"uploadSessionId\": \"$(echo "$UPLOAD_JSON" | jq -r '.uploadSessionId')\",
\"parameters\": { \"seed\": 128742 }
}"Create Edits
/v1/tools/{tool_id}/editSubmit edits with imageUrl, uploadSessionId, or a multipart image field. The response returns a job ID for polling, cancellation, deletion, and matching webhook events. Parameters a tool does not use are ignored.
AI Background
The AI background endpoint works best with a concise scene theme: a setting, surface, lighting cue, or mood in a few words. Good examples include warm oak tabletop,minimal white studio, and soft morning kitchen light.
curl -X POST https://api.photofig.com/v1/tools/ai-bg/edit \
-H "Authorization: Bearer pf_live_..." \
-H "Content-Type: application/json" \
-d '{
"imageUrl": "https://cdn.example.com/product.png",
"parameters": {
"theme": "warm oak tabletop",
"seed": 128742
}
}'Product Shadow
curl -X POST https://api.photofig.com/v1/tools/ai-shadows/edit \
-H "Authorization: Bearer pf_live_..." \
-H "Content-Type: application/json" \
-d '{
"imageUrl": "https://cdn.example.com/product.png",
"parameters": { "seed": 128742 }
}'Mask
mask takes no parameters and returns a PNG cutout with transparency.
curl -X POST https://api.photofig.com/v1/tools/mask/edit \
-H "Authorization: Bearer pf_live_..." \
-H "Content-Type: application/json" \
-d '{
"imageUrl": "https://cdn.example.com/product.png"
}'Resize
width and height set the output in pixels, from 100 to 4096. mode is fit (the whole photo, the default), fill (covers the canvas and crops the overflow), width, or height. horizontal and vertical place the photo, and background fills any space it leaves: transparent, a #RRGGBB color, or a gradient object. The result is a PNG.
curl -X POST https://api.photofig.com/v1/tools/resize/edit \
-H "Authorization: Bearer pf_live_..." \
-H "Content-Type: application/json" \
-d '{
"imageUrl": "https://cdn.example.com/product.jpg",
"parameters": {
"width": 1000,
"height": 1000,
"mode": "fit",
"background": "#ffffff"
}
}'Jobs
/v1/jobs/v1/jobs/{job_id}/v1/jobs/{job_id}/cancel/v1/jobs/{job_id}List, poll, cancel, or delete API jobs. API jobs do not appear in logged-in Uploads or Designs, but deletion removes API-owned artifacts.
GET /v1/jobs/{job_id} returns the job's status. When it is completed, download the image from resultUrl before expiresAt. Each status check counts toward your hourly allowance; use webhooks for completion events.
{
"id": "api_edit_uuid",
"tool": "ai-shadows",
"status": "completed",
"createdAt": "2026-07-15T10:15:00Z",
"startedAt": "2026-07-15T10:15:04Z",
"finishedAt": "2026-07-15T10:15:23Z",
"statusUrl": "https://api.photofig.com/v1/jobs/api_edit_uuid",
"resultUrl": "https://storage.example.com/signed-result-url",
"thumbnailUrl": "https://storage.example.com/signed-thumbnail-url",
"expiresAt": "2026-07-15T11:15:23Z",
"error": null
}Templates
/v1/templates/v1/templates/v1/templates/{template_id}/v1/templates/{template_id}/v1/templates/{template_id}A template is a look you apply to many photos: background removal, placement, a background and shadow, padding, and the sizes to export. Templates made over the API open and edit in the PhotoFig app, and the other way round. System templates are ready to use and read-only.
mask and resize are required; the other steps are optional and run in a fixed order. Unknown steps or fields are refused, and values must be within the ranges the editor offers. The OpenAPI schema lists every field.
curl -X POST https://api.photofig.com/v1/templates \
-H "Authorization: Bearer pf_live_..." \
-H "Content-Type: application/json" \
-d '{
"name": "Cream catalog",
"steps": {
"mask": {},
"reposition": { "horizontal": "center", "vertical": "center", "scale": 0.85 },
"background": { "backgroundType": "solid", "color": "#f6f1ea" },
"ai-shadows": { "opacity": 0.8 },
"resize": { "mode": "fit", "targets": [{ "width": 2000, "height": 2000 }] }
},
"export": { "format": "jpg", "background": "#ffffff" }
}'creditsPerPhoto is what the template charges for each photo, for example one AI credit for ai-shadows.
Batches
/v1/batches/v1/batches/{batch_id}/uploads/v1/batches/{batch_id}/uploads/{upload_id}/complete/v1/batches/{batch_id}/process/v1/batches/{batch_id}/v1/batches/v1/batches/{batch_id}/cancel/v1/batches/{batch_id}A batch applies one template to many photos. Create it, add the photos, then process it with a template. Photos keep the position you give them.
Each photo goes directly to storage: ask for an upload, PUT the bytes to uploadUrl, then complete it.
BATCH_ID=$(curl -s -X POST https://api.photofig.com/v1/batches \
-H "Authorization: Bearer pf_live_..." \
-H "Content-Type: application/json" \
-d '{ "name": "Spring arrivals" }' | jq -r '.id')
# For each photo: ask for an upload, PUT the bytes, complete it.
UPLOAD=$(curl -s -X POST "https://api.photofig.com/v1/batches/$BATCH_ID/uploads" \
-H "Authorization: Bearer pf_live_..." \
-H "Content-Type: application/json" \
-d '{ "filename": "boots.jpg", "contentType": "image/jpeg", "sizeBytes": 1842031, "position": 0 }')
curl -X PUT "$(echo "$UPLOAD" | jq -r '.uploadUrl')" \
-H "Content-Type: image/jpeg" --data-binary @boots.jpg
curl -X POST "https://api.photofig.com/v1/batches/$BATCH_ID/uploads/$(echo "$UPLOAD" | jq -r '.uploadId')/complete" \
-H "Authorization: Bearer pf_live_..."
curl -X POST "https://api.photofig.com/v1/batches/$BATCH_ID/process" \
-H "Authorization: Bearer pf_live_..." \
-H "Content-Type: application/json" \
-d '{ "templateId": "TEMPLATE_ID" }'Know when a batch finishes
Register a webhook for batch.completed once instead of polling. PhotoFig calls it when each batch finishes, with the batch ID and how many photos succeeded or failed.
curl -X POST https://api.photofig.com/v1/webhooks \
-H "Authorization: Bearer pf_live_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/photofig/webhooks",
"events": ["batch.completed"]
}'{
"event": "batch.completed",
"eventId": "uuid",
"createdAt": "2026-10-06T12:00:00Z",
"data": {
"batchId": "uuid",
"status": "completed",
"totalItems": 12,
"completedItems": 12,
"failedItems": 0
}
}When batch.completed arrives, read the batch. Each finished photo has a preview link, up to 1024 pixels. Background removals and AI credits are charged as in the app.
{
"id": "uuid",
"name": "Spring arrivals",
"status": "completed",
"templateName": "Cream catalog",
"totalItems": 2,
"completedItems": 2,
"failedItems": 0,
"resultsAvailableUntil": "2026-10-20T12:00:00Z",
"items": [
{
"id": "uuid",
"position": 0,
"filename": "boots.jpg",
"status": "completed",
"width": 3024,
"height": 4032,
"preview": {
"url": "https://...",
"width": 1024,
"height": 1024
}
}
]
}Download the results
/v1/batches/{batch_id}/items/{item_id}/download/v1/batches/{batch_id}/download-urlDownload each full-size result in the template's export format, or the whole batch as a ZIP. Each result counts once as an export, as in the app; downloading it again is free.
Share the results
/v1/batches/{batch_id}/share/v1/batches/{batch_id}/share/v1/batches/{batch_id}/shareA share link is a public page with before and after previews and a ZIP download, with nothing to sign in to. A batch has one link: asking again returns the same one, and revoking it ends it at once. Downloads from the page count as your exports, and the page works until availableUntil. Sharing needs a paid plan.
curl -X POST "https://api.photofig.com/v1/batches/$BATCH_ID/share" \
-H "Authorization: Bearer pf_live_..."{
"url": "https://photofig.com/share/TOKEN",
"availableUntil": "2026-10-20T12:00:00Z",
"createdAt": "2026-10-06T12:05:00Z",
"downloadCount": 0
}Webhooks
Webhooks let PhotoFig tell your server when an API job finishes, fails, or is cancelled, and when a batch finishes, so your backend does not need to poll. Register an HTTPS URL on a public host. An endpoint receives events for jobs and batches created with its owner's API keys.
/v1/webhooks/v1/webhooks/v1/webhooks/{webhook_id}/v1/webhooks/{webhook_id}/v1/webhooks/{webhook_id}/v1/webhooks/{webhook_id}/testSupported events are webhook.test,job.completed,job.failed,job.cancelled, and batch.completed. Job events use the job ID returned when the edit is created; batch.completed carries the batch ID.
{
"event": "job.completed",
"eventId": "uuid",
"createdAt": "2026-07-15T10:15:23Z",
"data": {
"id": "api_edit_uuid",
"tool": "ai-shadows",
"status": "completed",
"resultUrl": "https://storage.example.com/signed-result-url",
"thumbnailUrl": "https://storage.example.com/signed-thumbnail-url",
"expiresAt": "2026-07-15T11:15:23Z"
}
}Verify each delivery
Every request carries PhotoFig-Webhook-Timestamp and PhotoFig-Webhook-Signature. Compute an HMAC-SHA256 of the timestamp, a dot, and the raw request body with the endpoint's secret, and compare it with the value after v1=. Reject timestamps more than a few minutes old. The secret is shown only when the endpoint is created.
import crypto from 'node:crypto';
// rawBody is the request body exactly as received, before JSON parsing.
export function verifyPhotoFigWebhook(headers, rawBody, secret) {
const timestamp = headers['photofig-webhook-timestamp'];
const signature = headers['photofig-webhook-signature'] ?? '';
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = 'v1=' + crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
return signature.length === expected.length
&& crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}Respond with any 2xx status within 10 seconds. Other responses are retried after 1 minute, 5 minutes, 30 minutes, 2 hours, and 12 hours. PhotoFig-Webhook-Id stays the same across retries, so use it to skip repeats. Download links in the payload are signed for each attempt and stop working when the result expires.
Errors
Error responses use a stable machine-readable code and a human-readable message. Treat the code as the value to branch on in client logic.
{
"code": "invalid_request",
"message": "imageUrl or uploadSessionId is required"
}Common codes
invalid_request- A parameter is missing or invalid; the message says which.
api_key_not_allowed- An API key was used on a route outside the public API.
api_access_disabled- The workspace's plan does not include API access.
insufficient_funds- No AI credits or background removals are left for this tool.
rate_limit_exceeded- The hourly request allowance is used up; wait for Retry-After.
too_many_jobs- Too many jobs are queued or running; retry when earlier ones finish.
tool_unavailable- The tool's workers are briefly unavailable; retry shortly.
webhook_limit_reached- The key's owner has reached the webhook endpoint limit for this workspace.
Result Lifecycle
Completed API jobs stay visible through GET /v1/jobs and GET /v1/jobs/{job_id}. The status response contains signed resultUrl and thumbnailUrl links while the outputs are available.
API artifacts are stored separately from interactive uploads and designs. They are not shown in the app library.
API job records and API-owned input, result, and thumbnail files are retained for 1 hour after the job reaches completed, failed, or cancelled. The expiresAt field shows the scheduled cleanup time for finished jobs.
Use DELETE /v1/jobs/{job_id} after downloading results you no longer need. It cancels unfinished work when possible and deletes the job record plus API-owned artifacts before the automatic cleanup window.