Self-hosting

unbagrnd API docs

Everything in the desktop app, over HTTP — run your own background-removal API with Docker. Same on-device model, same privacy guarantee: no image you send it is ever sent anywhere but your own server.

Overview

unbagrnd-api is a self-hosted REST API wrapping the exact same on-device background-removal core the desktop app uses. There's no cloud, no account, and no unbagrnd-operated service in the loop — every image is processed by the model running inside your own container and never leaves it.

AuthX-API-Key header (per-request), X-Admin-Key for key management
Response formatRaw PNG for a single image, JSON for batch/keys/health
Interactive docsGET /docs — Swagger UI, generated from the same OpenAPI schema this page describes
StorageSQLite — API key metadata only, never image data
LicenseMIT, same as the desktop app
Nothing you upload is stored. Every request is processed in memory and discarded — not written to disk, not logged.

Quick start

One command with Docker Compose — no Rust toolchain, no manual ONNX Runtime setup.

git clone https://github.com/zidniryi/unbagrnd.git
cd unbagrnd
cp .env.example .env
# edit .env and set UNBAGRND_ADMIN_KEY, e.g:
#   openssl rand -hex 32
docker compose up --build

Once it's up:

curl http://localhost:8080/health
# {"status":"ok"}

On first startup the server downloads the configured model (~44 MB for the default, silueta) into the /models volume — that only happens once. Interactive docs are at http://localhost:8080/docs, and a ready-made Postman collection is in the repo.

Without Docker

cp .env.example .env   # set UNBAGRND_ADMIN_KEY
cargo run -p unbagrnd-api
UNBAGRND_MODELS_DIR and DATABASE_URL default to /models / /data, which won't be writable outside a container — override both to local paths (e.g. ./tmp/models, sqlite://./tmp/unbagrnd.db) for a bare-metal run.

Environment variables

Set in .env — see .env.example in the repo for the full, commented list.

VariableDefaultMeaning
HOST / PORT0.0.0.0 / 8080Where the server listens
DATABASE_URLsqlite:///data/unbagrnd.dbAPI key metadata storage
UNBAGRND_ADMIN_KEY(unset)Required for /v1/keys — see Authentication
UNBAGRND_MODEL_KEYsiluetaWhich background-removal model to load at startup
UNBAGRND_MODELS_DIR/modelsWhere the model is cached
UNBAGRND_MAX_FILE_SIZE_MB20Upload size limit, per image
UNBAGRND_MAX_IMAGE_WIDTH / HEIGHT8192Image dimension limit
UNBAGRND_MAX_CONCURRENT_INFERENCES2Bounds simultaneous inferences, regardless of how many requests are in flight
UNBAGRND_MAX_BATCH_SIZE10Max images per /v1/remove-background/batch request
UNBAGRND_RATE_LIMIT_PER_MINUTE60Per-API-key limit; 0 disables it; resets on restart
UNBAGRND_CORS_ORIGINS*Comma-separated allowlist, or *
RUST_LOGinfoStandard tracing-subscriber filter syntax

Authentication

Two separate credentials, two separate blast radiuses.

HeaderUsed forCan it manage keys?
X-API-Key/v1/remove-background, /v1/remove-background/batchNo — 401 on /v1/keys
X-Admin-Key/v1/keys (create / list / revoke)Yes — the only credential that can

If UNBAGRND_ADMIN_KEY isn't set, every /v1/keys route responds 503 ADMIN_KEY_NOT_CONFIGURED rather than falling back to some default credential — key management is simply off until you set one.

Creating an API key

curl -X POST http://localhost:8080/v1/keys \
  -H "X-Admin-Key: $UNBAGRND_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "my-production-key"}'
{
  "id": "7ae43488-...",
  "name": "my-production-key",
  "api_key": "unb_live_2b66742368e47512...",
  "created_at": "2026-01-01T00:00:00Z"
}
api_key is only ever returned here, at creation time. Only its SHA-256 hash is stored — save it now, there's no way to retrieve it again later.
GET/health

Health check

No auth required. Used by the Docker HEALTHCHECK and any uptime monitor.

curl http://localhost:8080/health
# {"status":"ok"}

Key management

All three require X-Admin-Key.

POST/v1/keys

Create a new API key. See the request/response example in Authentication above.

GET/v1/keys

List key metadata — never the key itself or its hash.

curl http://localhost:8080/v1/keys -H "X-Admin-Key: $UNBAGRND_ADMIN_KEY"
{
  "keys": [
    {
      "id": "7ae43488-...",
      "name": "my-production-key",
      "key_prefix": "unb_live_2b667423",
      "created_at": "2026-01-01T00:00:00Z",
      "last_used_at": "2026-01-02T09:14:00Z",
      "revoked_at": null,
      "request_count": 42
    }
  ]
}
DELETE/v1/keys/{id}

Revoke a key immediately. Soft-deleted (revoked_at stays as an audit trail) — a revoked key stops authenticating on its very next request. Revoking an already-revoked or unknown id returns 404.

curl -X DELETE http://localhost:8080/v1/keys/7ae43488-... \
  -H "X-Admin-Key: $UNBAGRND_ADMIN_KEY"
POST/v1/remove-background

Remove background

The actual product. One image in, one transparent PNG out.

curl -X POST http://localhost:8080/v1/remove-background \
  -H "X-API-Key: unb_live_xxxxxxxxx" \
  -F "image=@photo.jpg" \
  --output result.png

Multipart form field name is image (singular). Accepts PNG, JPEG, WebP, BMP, TIFF and GIF — the bytes are sniffed rather than trusting a filename or declared content type. Returns image/png with a transparent alpha channel.

Responses

StatusMeaning
200Transparent PNG body
400Missing/unreadable image field
401Missing/invalid X-API-Key
413Image exceeds UNBAGRND_MAX_FILE_SIZE_MB
422Background removal failed
429Rate limit exceeded
POST/v1/remove-background/batch

Batch removal

Up to UNBAGRND_MAX_BATCH_SIZE images (default 10) in one request.

curl -X POST http://localhost:8080/v1/remove-background/batch \
  -H "X-API-Key: unb_live_xxxxxxxxx" \
  -F "images=@photo1.jpg" \
  -F "images=@photo2.jpg" \
  -F "images=@photo3.jpg"

Multipart field name is images (plural) — repeat it once per file. Always returns 200 with one result per image, in upload order: a single corrupt or unreadable file becomes a per-item error rather than failing the whole batch.

{
  "results": [
    { "filename": "photo1.jpg", "status": "ok", "image_base64": "iVBORw0KG..." },
    { "filename": "photo2.jpg", "status": "ok", "image_base64": "iVBORw0KG..." },
    { "filename": "photo3.jpg", "status": "error",
      "error": { "code": "UNSUPPORTED_IMAGE", "message": "unsupported or unrecognized image format" } }
  ]
}

image_base64 is a standard base64-encoded transparent PNG — decode it to get the same file the single-image endpoint returns directly. Every item still goes through the same UNBAGRND_MAX_CONCURRENT_INFERENCES semaphore a single request does, so a large batch queues internally rather than spiking resource usage.

Responses

StatusMeaning
200Per-image results — see above (individual items can still report an error)
400No images field, or more files than UNBAGRND_MAX_BATCH_SIZE
401Missing/invalid X-API-Key
413One of the images exceeds UNBAGRND_MAX_FILE_SIZE_MB
429Rate limit exceeded

All endpoints at a glance

Errors

Always the same JSON shape, whatever the endpoint:

{
  "error": {
    "code": "INVALID_API_KEY",
    "message": "invalid or revoked API key"
  }
}
CodeStatusMeaning
MISSING_API_KEY401No X-API-Key header sent
INVALID_API_KEY401Unknown or revoked key
ADMIN_KEY_REQUIRED401No/wrong X-Admin-Key on a /v1/keys route
ADMIN_KEY_NOT_CONFIGURED503UNBAGRND_ADMIN_KEY isn't set on the server
KEY_NOT_FOUND404No such key id to revoke
INVALID_IMAGE400Missing field, empty body, or bad dimensions
UNSUPPORTED_IMAGE400Bytes don't decode as a supported image format
IMAGE_TOO_LARGE413Exceeds UNBAGRND_MAX_FILE_SIZE_MB
NO_IMAGES400Batch request with no images field
BATCH_TOO_LARGE400More files than UNBAGRND_MAX_BATCH_SIZE
INFERENCE_ERROR422The model failed to process a valid image
RATE_LIMITED429Too many requests for this key this minute
INTERNAL_ERROR500Unexpected server error (logged server-side, detail withheld from the response)

Rate limits & concurrency

Two independent knobs, both configurable per deployment.

  • Rate limiting — an in-memory, per-API-key fixed 60-second window, capped by UNBAGRND_RATE_LIMIT_PER_MINUTE (default 60). Resets on restart; 0 disables it entirely.
  • Inference concurrencyUNBAGRND_MAX_CONCURRENT_INFERENCES (default 2) bounds how many images are actually being processed by the model at once, across every request in flight (single or batch). Everything past that queues rather than piling onto the CPU/GPU.

Production deployment

A few things worth doing before exposing this to the public internet.

  • Put a reverse proxy (Caddy, nginx, Traefik, ...) in front for TLS — unbagrnd-api itself only speaks plain HTTP.
  • Restrict UNBAGRND_CORS_ORIGINS to your actual frontend origin(s) instead of *.
  • Treat UNBAGRND_ADMIN_KEY like any other production secret — a real secrets manager, not a checked-in .env.
  • Persist the /data and /models volumes (the bundled docker-compose.yml already does this) so a restart doesn't re-download the model or lose API key metadata.
Full source, the Dockerfile, and the Postman collection all live in the GitHub repo — this page mirrors the README's ## API section, kept in sync with it.