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.

  1. 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"
  2. 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"
  3. 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

GET/v1/account

Use 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

GET/v1/tools
GET/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

POST/v1/tools/{tool_id}/edit

This 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

POST/v1/uploads

Create 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

POST/v1/tools/{tool_id}/edit

Submit 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

GET/v1/jobs
GET/v1/jobs/{job_id}
POST/v1/jobs/{job_id}/cancel
DELETE/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

GET/v1/templates
POST/v1/templates
GET/v1/templates/{template_id}
PATCH/v1/templates/{template_id}
DELETE/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

POST/v1/batches
POST/v1/batches/{batch_id}/uploads
POST/v1/batches/{batch_id}/uploads/{upload_id}/complete
POST/v1/batches/{batch_id}/process
GET/v1/batches/{batch_id}
GET/v1/batches
POST/v1/batches/{batch_id}/cancel
DELETE/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

GET/v1/batches/{batch_id}/items/{item_id}/download
GET/v1/batches/{batch_id}/download-url

Download 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

POST/v1/batches/{batch_id}/share
GET/v1/batches/{batch_id}/share
DELETE/v1/batches/{batch_id}/share

A 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.

GET/v1/webhooks
POST/v1/webhooks
GET/v1/webhooks/{webhook_id}
PATCH/v1/webhooks/{webhook_id}
DELETE/v1/webhooks/{webhook_id}
POST/v1/webhooks/{webhook_id}/test

Supported 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.