The service accepts `multipart/form-data` for generation. Management routes also accept form data. Responses are JSON unless noted. The examples use `curl`; an agent can translate them to Python `requests` or `httpx`.
## Credentials and first-time setup
There are two kinds of credentials:
- **Admin secret**: configured by the service operator. Send it as `x-admin-secret` to administrator endpoints, or `auth-secret` to provider-key endpoints.
- **Client secret**: configured when creating a client. Send it as `x-client-secret` when generating or polling. It can also be used as `auth-secret` for that client's provider-key endpoints.
Add an upstream API key. Use `google` or `openrouter`; lower `priority` values are tried first within the selected provider ordering.
```sh
curl -sS -X POST "$BASE_URL/api/keys"\
-H "auth-secret: $ADMIN_SECRET"\
-F 'client_id=booth-01'\
-F 'provider=google'\
-F 'api_key=YOUR_GOOGLE_API_KEY'\
-F 'priority=1'
```
The same endpoint may be authorized with the owning client's secret instead of the admin secret. Add more provider keys at priorities 2, 3, and so on for fallback.
## Generate synchronously
`POST /v1/generate` holds the HTTP connection until the provider returns. Form fields:
`x-client-secret` is required for every generation and job-poll request. Clients without a configured secret are rejected. Uploads accept JPEG, PNG, or WebP only (not GIF or SVG), up to 8 files and 50 MiB aggregate by default. The service decodes and re-encodes each image before forwarding it, stripping metadata and non-image payloads, and rejects malformed images, mismatched MIME types, and images larger than 25 megapixels by default. Operators can tune the limits.
`cost_usd` is the amount reported by OpenRouter for that generation. The proxy requests OpenRouter usage metadata and returns it under `usage`; it also includes OpenRouter's request ID and routing/timing metadata when available. For providers that do not return a billed amount (currently Google native), `cost_usd` is `null` and `cost_source` is `unavailable`. Completed async jobs expose these fields inside `result`.
Generation responses also include `generation_id`, `request_intake_sec`, and `total_sec`. `request_intake_sec` covers the request reaching and being parsed by the proxy before the generation is queued; `elapsed_sec` is the upstream call time; `total_sec` runs from request receipt through completion. These measurements help separate slow client uploads from slow model calls.
Direct synchronous generation retains its JSON response with image bytes in `b64_json`. For async jobs, request `include_result=false` and use the binary download link described below. The legacy async `include_result=true` response remains supported and reconstructs Base64 from the stored image file.
Poll every couple of seconds. To avoid transferring the generated image inside every completed status response, set `include_result=false`. Include both the `client_id` query parameter and `x-client-secret` header:
On success with `include_result=false`, `status` is `completed` and `result` contains generation metadata plus image descriptors. Each image descriptor includes a `download_url` for binary images, or the upstream `url` when the provider supplies a hosted URL. Download each binary image directly; do not Base64-decode it:
Async generated images are stored as WebP at quality 90 by default (`OUTPUT_WEBP_QUALITY` can be set from 1 to 100). The image endpoint and result descriptor use `image/webp`; save the downloaded bytes with a `.webp` extension. Convert to PNG or JPEG in the client only if a downstream tool requires that format.
`GET /v1/jobs/{job_id}/images/{image_index}?token=...` serves raw image bytes from the proxy's private output directory with the corresponding image MIME type and inline content disposition. Links in the completed-job response are signed, scoped to that job and image, and expire after one hour by default (`IMAGE_LINK_TTL_SECONDS` configures the lifetime). Open the full `download_url` directly in a browser; no client-secret header is needed. Treat the URL like a temporary password: anyone who has it can view that image until it expires. An authenticated client may also call the image route with `client_id` and `x-client-secret` instead of a token. Unknown and cross-client jobs return `404`; an incomplete job returns `409`; expired links or deleted images return `410`.
Async WebP output files are stored under `OUTPUT_DIR` (default `./data/generated`) with directory mode `0700` and file mode `0600`. Image files are automatically deleted after 24 hours by default (`OUTPUT_RETENTION_SECONDS`), with a cleanup pass at startup and every five minutes. Job metadata, timing, and cost history remain in SQLite after image deletion; a deleted image endpoint returns `410`. Completed and failed jobs clear their original prompt and uploaded image payload from SQLite. The database retains only compact result metadata and execution history; startup migrates older completed outputs to WebP and vacuums SQLite to reclaim freed pages.
For compatibility, `include_result` defaults to `true`; that legacy mode includes Base64 image data in the status response. New clients should always request `include_result=false` and download images from their `download_url`. On failure, status is `failed` and `error` contains a sanitized message.
The built-in `nano-banana` alias maps to `gemini-2.5-flash-image` for Google and `google/gemini-2.5-flash-image` for OpenRouter by default. Operators can override these IDs with `GOOGLE_MODEL_NANO_BANANA` and `OPENROUTER_MODEL_NANO_BANANA`. Other model IDs pass through, with provider prefix normalization.
For OpenRouter, `reasoning` must be an object (for example `{"effort":"medium"}`); boolean values are omitted because the upstream API expects an object. Reasoning controls apply only to models that support them.
Provider APIs may reject unsupported parameters or model IDs. Configure exact IDs supported by the provider account. For a requested provider, fallback tries other active keys in configured priority order after a rate limit, server error, or connection/timeout failure. A non-retryable provider response stops the attempt sequence.
| `502` | Upstream failure after applicable failover attempts. Response contains status metadata, not upstream response bodies. |
## Notes for test scripts
The admin generations endpoint shows total, completed, and failed runs; sum of known costs; per-client cost and timing; and recent per-generation request intake, upstream, and total seconds. Prompts, images, and full provider keys are not included in this history.
- Aggregate cost and average timing fields include completed generations only. Failed runs remain visible in counts and recent history, but do not affect those totals or averages.
- Use a dedicated test client and avoid real production provider keys.
- A request with no configured provider key is useful for checking async job behavior: it should be accepted, then finish as `failed` with `No active provider keys configured`.
- A real image generation test requires an active provider key and can incur provider charges.
- The service currently has one in-process worker. Polling jobs survive client disconnects and completed results survive process restarts, but an active job is marked failed if the proxy restarts.