DEVELOPERS / API V1

Background removal API

Upload, remove, edit, export, and track images with one async API.

Quick start

The API is async by default. Send an image, get a job, then poll, stream events, or receive a signed webhook. Local base URL:

http://127.0.0.1:3001

Hosted workspaces receive an HTTPS URL. Bodies are JSON unless an endpoint uses multipart or binary data.

curl -X POST http://127.0.0.1:3001/v1/background-removals \
  -H "Authorization: Bearer $BLANKGROUND_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Prefer: wait=8" \
  -F "image=@product.jpg" \
  -F 'format=png' \
  -F 'recipe={"version":1,"background":{"type":"transparent"}}'

A fast job returns 200 OK. After eight seconds, you get 202 Accepted and a job URL in Location.

Service documents and health

Method and pathPurpose
GET /openapi.jsonOpenAPI 3.1 contract used to generate SDKs
GET /health/liveConfirms that the API process is alive
GET /health/readyReports whether required processing dependencies are ready

Authentication

Protected requests use a bearer API key:

Authorization: Bearer bg_live_••••••••••••••••••••

Keys appear once, are stored as hashes, and support scopes and expiry. Keep them server-side and out of URLs, logs, browsers, and support requests. Anonymous removal uses a signed device cookie and excludes full resolution, batches, keys, webhooks, and projects.

Common scopes:

ScopeAllows
images:writeUploads, removal jobs, batches, refinements, exports
jobs:readJob, event, result, history, and credit reads
webhooks:writeWebhook endpoint management
keys:writeAPI-key management

Request conventions

Idempotency

Creation endpoints require Idempotency-Key. Retry the same operation with the same key and body. Different input returns 409 idempotency_conflict.

Bounded waiting

Use Prefer: wait=8 to wait up to eight seconds. Success returns 200; otherwise you get 202.

States and stages

States: queued, processing, succeeded, failed, canceled. Stages: preparing, segmenting, refining, rendering, storing, complete.

Limits

LimitValue
Input formatsJPEG, PNG, WebP, AVIF
Minimum dimensions32×32
Maximum pixels25 megapixels
Maximum bytes50 MB
Batch size100 images
Remote redirects5
AI refinements20 per project
Result URL10 minutes
Binary retention24 hours after terminal completion

Source URLs must be public HTTP or HTTPS. Private networks, metadata services, credentials, unsafe DNS, and rebinding are blocked.

Uploads

Use direct uploads for large files and batches. Local mode returns an API path; persistent environments may return a presigned storage URL.

POST/v1/uploads

Creates a short-lived upload reservation. No body is required.

{
  "id": "019c…",
  "uploadUrl": "/v1/uploads/019c…/content",
  "expiresAt": "2026-08-05T12:15:00.000Z"
}
PUT/v1/uploads/{id}/content

Upload raw bytes with Content-Type. The server still validates the decoded image.

curl -X PUT "$BASE_URL/v1/uploads/$UPLOAD_ID/content" \
  -H "Authorization: Bearer $BLANKGROUND_API_KEY" \
  -H "Content-Type: image/jpeg" \
  --data-binary @product.jpg
POST/v1/uploads/{id}/complete

Inspect and confirm the upload. Returns verified dimensions, media type, and ready status.

Background removals

POST/v1/background-removals

Accepts multipart data with image, or JSON with exactly one of uploadId and sourceUrl.

Multipart parameters:

NameTypeRequiredDescription
imagefileyesJPEG, PNG, WebP, or AVIF
formatstringnopng, webp, jpeg, zip, or psd; default png
recipeJSON stringnoVersioned composition recipe

JSON request:

{
  "uploadId": "019c…",
  "format": "webp",
  "recipe": {
    "version": 1,
    "background": { "type": "transparent" },
    "crop": true,
    "margin": 48,
    "width": 1600,
    "height": 1600,
    "scale": 0.92,
    "positionX": 0.5,
    "positionY": 0.5,
    "quality": 88,
    "shadow": {
      "color": "#000000",
      "opacity": 0.22,
      "blur": 24,
      "distance": 14,
      "angle": 90
    }
  }
}

Response:

{
  "id": "019c…",
  "projectId": "019c…",
  "workspaceId": "workspace:…",
  "status": "queued",
  "stage": "preparing",
  "format": "png",
  "createdAt": "2026-08-05T12:00:00.000Z"
}

Jobs and results

GET/v1/jobs/{id}

Returns status, stage, timestamps, model version, result metadata, or a stable error.

GET/v1/jobs

Returns workspace history. Anonymous history requires a known job ID in the same device session.

GET/v1/jobs/{id}/events

Returns a text/event-stream. Event fields include id, type, occurredAt, workspaceId, aggregateId, correlationId, and data.

id: 019c…
event: image.job.completed.v1
data: {"id":"019c…","type":"image.job.completed.v1","data":{"jobId":"019c…"}}

Reconnect with the last event ID. After a gap, reconcile with GET /v1/jobs/{id}.

POST/v1/jobs/{id}/cancel

Cancel queued jobs immediately. Active jobs may briefly remain processing. Cancellation releases the credit hold.

GET/v1/jobs/{id}/result

Returns the result or a signed ten-minute URL. Expired files return 410 expired_asset; job metadata remains.

Batches

POST/v1/batches

Creates up to 100 removal jobs from completed upload IDs. Requires authentication and Idempotency-Key.

{
  "uploadIds": ["019c…", "019d…"],
  "format": "jpeg",
  "recipe": {
    "version": 1,
    "background": { "type": "color", "color": "#ffffff" },
    "crop": true,
    "margin": 64,
    "width": 1600,
    "height": 1600
  }
}

The response is 202 Accepted with a batch ID, total, status, and child jobs.

GET/v1/batches/{id}

Returns batch progress and child jobs. Partial failure keeps successful results.

Projects and editing

POST/v1/projects/{id}/revisions

Creates an immutable revision. A stale baseRevisionId returns 409 revision_conflict.

{
  "baseRevisionId": "019c…",
  "strokes": [
    {
      "tool": "restore",
      "points": [
        [0.42, 0.17],
        [0.43, 0.18]
      ],
      "size": 0.02,
      "hardness": 0.7,
      "opacity": 1
    }
  ],
  "recipe": { "version": 1, "background": { "type": "transparent" } }
}

Stroke coordinates and sizes are normalized to the source image so server-side rasterization is deterministic at original resolution.

POST/v1/projects/{id}/refinements

Queues point or box refinement. It shares the workspace slot, costs no extra credit, and is capped at 20 per project.

{
  "points": [
    { "x": 0.42, "y": 0.33, "label": "foreground" },
    { "x": 0.61, "y": 0.45, "label": "background" }
  ],
  "baseRevisionId": "019c…"
}
POST/v1/projects/{id}/exports

Renders a project revision with an optional recipe and png, webp, jpeg, zip, or psd format. Renders do not consume another removal credit.

Credits

GET/v1/credits

Workspace response:

{
  "kind": "workspace",
  "limit": 100,
  "available": 87,
  "held": 2,
  "used": 11,
  "periodEndsAt": "2026-09-01T00:00:00.000Z"
}

Anonymous responses show the 7-per-day balance and UTC reset. Credits are held on start, charged on success, and released on failure or cancellation.

DELETE/v1/device

Deletes this browser’s anonymous cookie. It does not delete an account or reset the allowance.

Email authentication

POST/v1/auth/email/start

Body: { "email": "person@example.com" }. Sends a six-digit, ten-minute, single-use code. Responses do not reveal whether an email already exists.

POST/v1/auth/email/verify

Body: { "email": "person@example.com", "code": "123456" }. Creates the default workspace when needed and returns the authenticated session representation. Five failed attempts invalidate the challenge.

API keys

GET/v1/api-keys

Lists key metadata—ID, name, prefix, scopes, creation, expiry, and last use—never the secret.

POST/v1/api-keys
{
  "name": "Catalog production",
  "scopes": ["images:write", "jobs:read"],
  "expiresAt": "2027-01-01T00:00:00.000Z"
}

Returns 201 Created and the secret once. Store it immediately.

DELETE/v1/api-keys/{id}

Revokes the key now. For rotation, create the new key before deleting the old one.

Webhooks

GET/v1/webhooks

Lists workspace webhook endpoints and delivery configuration without returning signing secrets.

POST/v1/webhooks
{
  "url": "https://example.com/hooks/blankground",
  "events": ["image.job.completed.v1", "image.job.failed.v1"]
}

The signing secret is displayed once. Public endpoints must use HTTPS.

DELETE/v1/webhooks/{id}

Disables delivery and removes the endpoint.

Verifying a delivery

Headers identify the timestamp, delivery ID, and signature. Compute HMAC-SHA256 over:

{timestamp}.{deliveryId}.{rawRequestBody}

Compare in constant time, reject stale timestamps, and store delivery IDs against replay. Return 2xx after durable acceptance. Retries continue for 24 hours.

import { createHmac, timingSafeEqual } from "node:crypto";

function verify(
  secret: string,
  timestamp: string,
  deliveryId: string,
  rawBody: string,
  supplied: string,
) {
  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.${deliveryId}.${rawBody}`)
    .digest("hex");
  return timingSafeEqual(Buffer.from(expected), Buffer.from(supplied));
}

Device authorization

Photoshop and other limited-input clients use a device-code flow.

POST/v1/device-authorizations

Body: { "client": "photoshop" }. Returns deviceCode, human-readable userCode, verification URI, expiry, and polling interval.

POST/v1/device-authorizations/{code}/approve

Called by an authenticated browser after the user confirms the code and workspace. The code is short lived and single use.

POST/v1/device-authorizations/token

Body: { "deviceCode": "…" }. Returns 428 authorization_pending until approval, then returns access and refresh credentials. Clients must respect the polling interval.

Errors

Errors use a stable machine-readable code and human-readable message:

{
  "error": {
    "code": "quota_exhausted",
    "message": "No full-resolution credits remain for this period.",
    "requestId": "019c…"
  }
}
HTTPCodeMeaning
400invalid_inputBody, parameters, image, or dimensions are invalid
401invalid_api_keyCredential is absent, invalid, expired, or revoked
403insufficient_scopeKey lacks the required scope
404job_not_foundResource does not exist in the authenticated workspace
409idempotency_conflictKey was reused with different input
409revision_conflictEdit base is stale
410expired_assetMetadata exists but result bytes expired
413image_too_largeByte or pixel limit exceeded
415unsupported_mediaFormat or signature is unsupported
422unsafe_urlRemote URL resolves to a prohibited target
428authorization_pendingDevice flow awaits approval
429quota_exhaustedAllowance or refinement cap exhausted
500processing_failedTerminal processor failure
503processor_unavailableNo healthy processing capacity

Retry 429 after reset. Retry idempotent 5xx requests with backoff and jitter. Fix invalid input before retrying.

SDK examples

The TypeScript and Python SDKs are generated from the same OpenAPI 3.1 document served at /openapi.json.

import { postV1BackgroundRemovals } from "@blankground/sdk";

const result = await postV1BackgroundRemovals({
  body: { image: file },
  headers: {
    Authorization: `Bearer ${process.env.BLANKGROUND_API_KEY}`,
    "Idempotency-Key": crypto.randomUUID(),
    Prefer: "wait=8",
  },
});
import os
from background_removal_api_client import AuthenticatedClient
from background_removal_api_client.api.jobs import post_v1_background_removals

client = AuthenticatedClient(
    base_url="http://127.0.0.1:3001",
    token=os.environ["BLANKGROUND_API_KEY"],
)

with open("product.jpg", "rb") as image:
    response = post_v1_background_removals.sync_detailed(
        client=client,
        body={"image": image},
        idempotency_key="catalog-item-184-v1",
    )

Generated method signatures track the committed OpenAPI document. Check the SDK README for the exact version’s multipart wrapper types.