API Guide

Working with the API

Everything Asset Hub does in the UI is a REST call you can make yourself — search your library, kick off generations, poll jobs, manage sets. This is the friendly overview; the machine-readable contract is the OpenAPI schema your instance serves.

Base URL

All endpoints live under /api/v1 on your own instance. The OpenAPI schema is the source of truth — if this guide and the schema ever disagree, the schema wins.

# local — through the UI's nginx
http://localhost:3000/api/v1
# local — backend direct
http://localhost:8000/api/v1
# production
https://<your-host>/api/v1

# the machine-readable contract (no token needed):
GET /api/v1/openapi.json
# interactive Swagger UI:
/api/v1/docs
Tip: point your generated client or curl at openapi.json — it always matches the version you're actually running.

Conventions

Authentication

Log in for a token pair, then send the access token on every request. The first registered user becomes an activated admin; everyone else needs admin approval.

POST /auth/login        {"email","password"}    {access_token, refresh_token}
POST /auth/refresh      {"refresh_token"}       new token pair
GET  /auth/me                                   UserResponse

# then, on every request:
Authorization: Bearer <access_token>

API keys (for machine clients)

For scripts or integrations (e.g. a Unity plugin), create a long-lived API key and use it exactly like a bearer token. The plaintext key is shown once.

POST   /auth/api-keys     {"name"}    201 {key: "ah_…"}   # copy it now
GET    /auth/api-keys                 {"items": [...]}
DELETE /auth/api-keys/{key_id}        204
SSO: with OIDC enabled, GET /auth/oidc/login starts the flow; set the IdP redirect URI to {PUBLIC_URL}/api/v1/auth/oidc/callback.

Generation

All generation is asynchronous and returns a job to poll. Send a prompt, pick a model/provider, optionally spread the work across several providers at once with distribution — every result lands in one Set for side-by-side comparison.

POST /items/generate         GenerateRequest (prompt, generator_model, provider?, distribution?)
POST /items/generate-upload  # multipart: files + prompt/category/provider…

     202 Accepted   Location: /api/v1/jobs/{job_id}
       JobInfo {job_id, status:"QUEUED", generation_mode, item_id, service}

# race several providers — one Set, results side by side:
{"distribution": {"comfyui": 2, "meshy": 1}}    + {group_id, job_ids}

Derive from an existing asset

Derive actions never mutate the source — each result is a new item in the source's Set.

POST /items/{id}/image-to-3d      {"provider"?, "distribution"?, "workflow_filename"?}
POST /items/{id}/reimagine        {"prompt"?, "provider"?, …}
POST /items/{id}/image-to-video   {"prompt"?, "duration_seconds"?, …}
POST /items/{id}/retexture        {"prompt"?, "provider"?, …}

# operate on many at once:
POST /items/bulk-generate         {"item_ids", "target":"3d"|"image"|"video"}    202

Jobs

After a 202, poll the job until it reaches a terminal state. Active jobs report their queue position; finished ones carry timing, progress and any error.

GET  /jobs?limit=50         {"items": [JobInfo]}   # queue_position while active
GET  /jobs/{job_id}         JobInfo (progress, stage, error_message, timestamps)
POST /jobs/{job_id}/retry  # re-queue a FAILED / CANCELED job
POST /jobs/{job_id}/cancel # 409 once it's already at the provider
The lifecycle: QUEUED → IN_PROGRESS → SUCCEEDED (preview → final), then the LLM auto-enriches metadata. On failure the item exposes a failed_job_id so the UI can offer a retry.

Sets & collections

Sets (groups) bundle related items — e.g. every variant from a race. Collections are the named tiles in the library sidebar.

GET  /groups                      {"items": [{id, name, member_count, modalities}]}
POST /groups                     {"name", "item_ids"?}    201
POST /groups/{id}/bulk-generate  {"target":"3d", …}       202

GET  /collections                 {"items": [tiles]}
PUT  /collections                {"collections":[tiles]}   # full replace, order = list

Share links

Share a single asset — or, as an admin, a whole collection — with people who don't have an account. Creating a share returns a public URL that is served by an isolated, credential-less share proxy on its own hostname, so anonymous visitors never touch the main app or this API.

POST   /items/{id}/shares         {"expires_in_days"?, "password"?, "allow_download"?}
        201 {url: "https://share.<your-host>/s/<token>", expires_at, …}
POST   /collections/{id}/shares   # admin only — collections span users
GET    /shares                      {"shares": [...]}   # yours (admins: everyone's)
DELETE /shares/{share_id}           204   # revoke — the link dies immediately
Security: how the share proxy isolates public traffic is covered on the security page.

Errors

Every error is application/problem+json — a predictable shape you can branch on.

GET /api/v1/items/999999
     404   Content-Type: application/problem+json
      {"type":"about:blank", "title":"Not Found",
       "status":404, "detail":"Item not found"}

# 401 additionally carries:  WWW-Authenticate: Bearer   (RFC 6750)
This page is the guided tour. For the exhaustive, always-current reference, open Swagger UI at /api/v1/docs on your instance, or read API.md in the repository.