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.
| Auth | X-API-Key header (per-request), X-Admin-Key for key management |
| Response format | Raw PNG for a single image, JSON for batch/keys/health |
| Interactive docs | GET /docs — Swagger UI, generated from the same OpenAPI schema this page describes |
| Storage | SQLite — API key metadata only, never image data |
| License | MIT, same as the desktop app |
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.
| Variable | Default | Meaning |
|---|---|---|
HOST / PORT | 0.0.0.0 / 8080 | Where the server listens |
DATABASE_URL | sqlite:///data/unbagrnd.db | API key metadata storage |
UNBAGRND_ADMIN_KEY | (unset) | Required for /v1/keys — see Authentication |
UNBAGRND_MODEL_KEY | silueta | Which background-removal model to load at startup |
UNBAGRND_MODELS_DIR | /models | Where the model is cached |
UNBAGRND_MAX_FILE_SIZE_MB | 20 | Upload size limit, per image |
UNBAGRND_MAX_IMAGE_WIDTH / HEIGHT | 8192 | Image dimension limit |
UNBAGRND_MAX_CONCURRENT_INFERENCES | 2 | Bounds simultaneous inferences, regardless of how many requests are in flight |
UNBAGRND_MAX_BATCH_SIZE | 10 | Max images per /v1/remove-background/batch request |
UNBAGRND_RATE_LIMIT_PER_MINUTE | 60 | Per-API-key limit; 0 disables it; resets on restart |
UNBAGRND_CORS_ORIGINS | * | Comma-separated allowlist, or * |
RUST_LOG | info | Standard tracing-subscriber filter syntax |
Authentication
Two separate credentials, two separate blast radiuses.
| Header | Used for | Can it manage keys? |
|---|---|---|
X-API-Key | /v1/remove-background, /v1/remove-background/batch | No — 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.
/healthHealth 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.
/v1/keysCreate a new API key. See the request/response example in Authentication above.
/v1/keysList 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
}
]
}
/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"
/v1/remove-backgroundRemove 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
| Status | Meaning |
|---|---|
200 | Transparent PNG body |
400 | Missing/unreadable image field |
401 | Missing/invalid X-API-Key |
413 | Image exceeds UNBAGRND_MAX_FILE_SIZE_MB |
422 | Background removal failed |
429 | Rate limit exceeded |
/v1/remove-background/batchBatch 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
| Status | Meaning |
|---|---|
200 | Per-image results — see above (individual items can still report an error) |
400 | No images field, or more files than UNBAGRND_MAX_BATCH_SIZE |
401 | Missing/invalid X-API-Key |
413 | One of the images exceeds UNBAGRND_MAX_FILE_SIZE_MB |
429 | Rate 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"
}
}
| Code | Status | Meaning |
|---|---|---|
MISSING_API_KEY | 401 | No X-API-Key header sent |
INVALID_API_KEY | 401 | Unknown or revoked key |
ADMIN_KEY_REQUIRED | 401 | No/wrong X-Admin-Key on a /v1/keys route |
ADMIN_KEY_NOT_CONFIGURED | 503 | UNBAGRND_ADMIN_KEY isn't set on the server |
KEY_NOT_FOUND | 404 | No such key id to revoke |
INVALID_IMAGE | 400 | Missing field, empty body, or bad dimensions |
UNSUPPORTED_IMAGE | 400 | Bytes don't decode as a supported image format |
IMAGE_TOO_LARGE | 413 | Exceeds UNBAGRND_MAX_FILE_SIZE_MB |
NO_IMAGES | 400 | Batch request with no images field |
BATCH_TOO_LARGE | 400 | More files than UNBAGRND_MAX_BATCH_SIZE |
INFERENCE_ERROR | 422 | The model failed to process a valid image |
RATE_LIMITED | 429 | Too many requests for this key this minute |
INTERNAL_ERROR | 500 | Unexpected 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;0disables it entirely. - Inference concurrency —
UNBAGRND_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-apiitself only speaks plain HTTP. - Restrict
UNBAGRND_CORS_ORIGINSto your actual frontend origin(s) instead of*. - Treat
UNBAGRND_ADMIN_KEYlike any other production secret — a real secrets manager, not a checked-in.env. - Persist the
/dataand/modelsvolumes (the bundleddocker-compose.ymlalready does this) so a restart doesn't re-download the model or lose API key metadata.
## API section, kept in sync with it.