# Imgen API v2 — LLM Reference

Concise reference optimized for code-generation agents (Claude Code etc.). For the full human-facing docs see `https://imgen.api.efficientstack.com/docs`.

## Endpoint

`POST https://imgen.api.efficientstack.com/api/v2`

All requests:
- Method: `POST`
- Header: `Content-Type: application/json`
- Header: `Authorization: Bearer <API-KEY>` (required; 401 if missing/invalid/disabled)
- Body: JSON with an `action` field

## Actions

| action | purpose |
|---|---|
| `models` | list available characters, models, style presets |
| `generate` | submit an image generation job, returns a job id |
| `status` | poll a job until terminal state |
| `optimize` | LLM-enhance a prompt without generating |
| `random` | server-side random prompt builder |

## Prompt Syntax

The `prompt` field is a raw string. The server tokenizes:

- `@name` — activate a character LoRA (one per prompt). Unknown name is ignored.
- `-term` — comma-separated negative token. Multiple allowed.
- Everything else — positive prompt text.

Example: `@aya black lingerie, on bed, warm lighting, -red dress, -ugly hands`

## action: models

Request:
```json
{ "action": "models" }
```

Response:
```json
{
  "characters": ["aya", "bella", "..."],
  "models": {
    "eros": {
      "label": "Eros",
      "default_style": "professional",
      "styles": { "professional": { "label": "Professional" } }
    }
  }
}
```

## action: generate

Request fields:

| field | type | required | default | notes |
|---|---|---|---|---|
| `action` | string | yes | — | `"generate"` |
| `prompt` | string | yes | — | raw prompt incl. `@char` / `-neg` |
| `model` | string | no | first configured | model key from `models` |
| `negative` | string | no | `""` | extra negatives |
| `optimize` | bool | no | `false` | LLM-enhance prompt before gen |
| `use_cache` | bool | no | `false` | try semantic cache first |
| `cache_only` | bool | no | `false` | 404 on cache miss instead of generating |
| `cache_force` | bool | no | `false` | ignore 0.95 similarity floor |
| `cache_pick` | int 1–10 | no | `5` | top-N pool size for cache random pick |
| `width` | int | no | `1344` | must be a valid pair (see below) |
| `height` | int | no | `768` | must be a valid pair (see below) |
| `style` | string | no | model default | style preset key |
| `format` | string | no | `"jpeg"` | `jpeg` \| `png` \| `webp` \| `avif` |
| `quality` | int 1–100 | no | `95` | compression quality |
| `lossless` | bool | no | `false` | webp/avif lossless |
| `compliance_ruleset` | string | no | `"default"` | ACS ruleset to apply |
| `precompliance` | string ≤2000 | no | `""` | pre-screening result from upstream keyword filter (Ban Word Service). Forwarded to ACS as supplementary context, NOT authoritative |
| `wm_image` | string | no | `""` | watermark as base64 data URI |
| `wm_position` | string | no | `"bottom-right"` | `top-left` \| `top-right` \| `center` \| `bottom-left` \| `bottom-right` |
| `wm_scale` | int 1–100 | no | `5` | watermark scale |
| `wm_transparency` | int 0–100 | no | `100` | watermark opacity |
| `wm_rotation` | int -360–360 | no | `0` | watermark rotation deg |
| `wm_padding_x` | int 0–500 | no | `10` | watermark x padding |
| `wm_padding_y` | int 0–500 | no | `10` | watermark y padding |
| `cs_author` | string | no | `""` | C2PA override |
| `cs_title` | string | no | `""` | C2PA override |
| `cs_description` | string | no | `""` | C2PA override |
| `cs_organization` | string | no | `""` | C2PA override |
| `cs_vendor` | string | no | `""` | C2PA override |

Valid dimensions (anything else falls back to `1344x768`):
`1024x1024`, `1344x768`, `768x1344`, `1216x832`, `832x1216`, `1152x896`, `896x1152`

Silent fallbacks: invalid model → first model; invalid style → model default; invalid format → `jpeg`; bad dimensions → `1344x768`; empty/whitespace `compliance_ruleset` → `"default"`; out-of-range `cache_pick` → clamped to `[1,10]`; `precompliance` longer than 2000 chars is truncated.

Response:
```json
{
  "id": "run_abc123",
  "prompt": "@aya enhanced positive text, -red dress",
  "model": "eros",
  "format": "jpeg",
  "gen_id": "a1b2c3d4e5f6g7h8i9j0"
}
```

Cache hit (only when `use_cache: true` and an eligible match exists):
```json
{
  "id": "cache_<hex>",
  "prompt": "...",
  "model": "eros",
  "format": "jpeg",
  "gen_id": "...",
  "cache": true,
  "matches": [
    { "id": "img_2", "url": "https://...alt1.jpg?...", "thumbnail": "https://...alt1_thumb.jpg?...", "width": 1344, "height": 768, "score": 0.981 }
  ]
}
```
Cache hits expire 1 hour after `generate`. Poll `status` promptly. `cache_only: true` + miss → HTTP 404 `{"error":"No cached result found"}`.

`matches` = the OTHER (non-selected) candidates from the same top-`cache_pick` pool the returned image was randomly drawn from. Each entry: `id`, `url`, `thumbnail`, `width`, `height`, `score`. Purely additive and backward-compatible — the primary image is unchanged. Because the primary image is picked at random, it can occasionally repeat across requests; if you detect a repeat, choose an alternative URL from `matches` rather than re-requesting. All entries share the request's `model`/`style`/`character`. Empty when only one candidate was eligible. Only present on cache hits.

## action: status

Always pass the original `id`, `prompt`, and `model` from the `generate` response.

```json
{ "action": "status", "id": "run_abc123", "prompt": "<from generate>", "model": "eros" }
```

Possible responses:

Pending:
```json
{ "status": "IN_QUEUE" }
{ "status": "IN_PROGRESS" }
```

Completed (safe):
```json
{
  "status": "COMPLETED",
  "meta": {
    "id": "...",
    "gen_id": "...",
    "time": "2026-05-28T20:02:50Z",
    "prompt": "...",
    "positive": "...",
    "negative": "...",
    "character": "aya",
    "model": "eros",
    "style": "professional",
    "width": 1344, "height": 768,
    "format": "jpeg", "quality": 95, "lossless": false,
    "optimized": true,
    "compliance": "safe",
    "compliance_codes": [],
    "compliance_ruleset": "default",
    "precompliance": "",
    "fingerprint": "...",
    "images": [
      { "url": "https://...signed.jpg?...", "filename": "...", "type": "signed" },
      { "url": "https://...thumb.jpg?...", "filename": "...", "type": "thumbnail" }
    ],
    "...": "..."
  }
}
```

Completed (cache hit) — identical shape, plus `"cache": true`, always `compliance: "safe"`, plus a top-level `"matches"` array (also mirrored at `meta.matches`). Same semantics as the `generate` cache-hit `matches` field above.

Completed (blocked by ACS):
```json
{
  "status": "COMPLETED",
  "compliance": "unsafe",
  "error": "Content blocked — flagged for: <category names>",
  "filter_categories": ["<acs-rule-id>", "..."],
  "unsafe_url": "<base64-encoded signed S3 URL>"
}
```
Decode `unsafe_url` with `atob()` (JS) or `base64.b64decode()` (Python) — the asset URL is intentionally base64-wrapped to discourage casual display.

Terminal failure:
```json
{ "status": "FAILED" | "TIMED_OUT" | "CANCELLED", "error": "Generation failed" }
```

## action: optimize

```json
{ "action": "optimize", "prompt": "@aya black lingerie, on bed", "model": "eros" }
```
Returns `{ "prompt": "<enhanced>" }`. Returns the original prompt on a no-op enhancement; `{ "error": "Enhancement failed" }` (502) on upstream LLM error.

## action: random

```json
{ "action": "random" }                   // random char (or none if none configured)
{ "action": "random", "character": "" }  // explicitly no character
{ "action": "random", "character": "aya" } // specific character (ignored if unknown)
```
Returns `{ "prompt": "@aya ..." }`.

## Server-side Prompt Construction

Positive sent to the diffusion model:
```
[trigger], [character_positive], <user prompt>[, <style quality tags>]
```

Negative sent to the diffusion model:
```
<base negative>[, <style negative_extra>][, <character negative>][, <parsed -terms>][, <explicit negative>]
```

## Compliance

Every non-cached generation is reviewed by ACS during `status` when the underlying job hits `COMPLETED`. Cached jobs are pre-cleared and skip review.

- `compliance_ruleset` selects which ACS ruleset applies (persisted on the job; not needed on poll).
- `precompliance` is appended to the ACS context as a clearly labelled supplementary signal. ACS still makes the authoritative call. Use it to forward pre-screening output to give ACS more context without overriding it.

## Caching (`use_cache: true`)

1. Semantic search the cache with the parsed positive prompt.
2. Strict equality required on `model`, `style`, `character`.
3. Default eligibility floor: similarity > 0.95. Disable with `cache_force: true`.
4. One random pick from top-`cache_pick` (default 5, clamped to `[1, 10]`). The remaining pool members are returned in `matches`.
5. Cache hits bypass `optimize` and ACS review.
6. Cache miss with `cache_only: false` (default) → falls through to normal generation.
7. Cache miss with `cache_only: true` → HTTP 404 `{"error":"No cached result found"}`.
8. `matches` (cache hits only) exposes the other non-selected candidates so clients can pick an alternative if they detect the randomly-selected image is a repeat.

## Polling Strategy (recommended)

- Interval: 3s
- Max polls: 120 (≈6 min)
- Wait for each poll to complete before issuing the next (no overlap)
- Terminal states: `COMPLETED`, `FAILED`, `TIMED_OUT`, `CANCELLED`
- On `COMPLETED`, always check `compliance` before reading `meta`

## Errors

| HTTP | error | cause |
|---|---|---|
| 400 | `Missing action` | no `action` field |
| 400 | `Prompt is required` | empty prompt |
| 400 | `Invalid character` | unknown `@name` referenced in prompt |
| 400 | `Bad ID` | invalid job id format on `status` |
| 401 | `Unauthorized` / `Invalid API key` / `API key is disabled` | auth failure |
| 404 | `No cached result found` | `use_cache` + `cache_only` with no match |
| 404 | `Cached result not found or expired` | cache job id expired (>1h) |
| 502 | `Generation failed` | upstream RunPod error |
| 502 | `Enhancement failed` | upstream LLM error in `optimize` |

## Minimal JavaScript Client

```javascript
const API = 'https://imgen.api.efficientstack.com/api/v2';
const TOKEN = process.env.IMGEN_API_KEY;

async function api(body) {
  const r = await fetch(API, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${TOKEN}` },
    body: JSON.stringify(body),
  });
  return r.json();
}

async function generate(prompt, opts = {}) {
  const job = await api({
    action: 'generate',
    prompt,
    model: opts.model || 'eros',
    style: opts.style || '',
    use_cache: !!opts.use_cache,
    compliance_ruleset: opts.compliance_ruleset || '',
    precompliance: opts.precompliance || '',
  });
  if (job.error) throw new Error(job.error);

  for (let i = 0; i < 120; i++) {
    await new Promise(r => setTimeout(r, 3000));
    const s = await api({ action: 'status', id: job.id, prompt: job.prompt, model: job.model });
    if (s.status === 'IN_QUEUE' || s.status === 'IN_PROGRESS') continue;
    if (s.status === 'COMPLETED') {
      if (s.compliance === 'unsafe') {
        const blockedUrl = s.unsafe_url ? atob(s.unsafe_url) : null;
        const err = new Error(s.error);
        err.unsafeUrl = blockedUrl;
        err.categories = s.filter_categories || [];
        throw err;
      }
      return { meta: s.meta, fromCache: !!s.cache, matches: s.matches || [] };
    }
    throw new Error(s.error || 'Failed: ' + s.status);
  }
  throw new Error('Polling timed out');
}

// Example with pre-compliance forwarded from an upstream Ban Word Service:
const { meta, fromCache, matches } = await generate('@aya black lingerie, warm lighting', {
  style: 'professional',
  use_cache: true,
  precompliance: 'ban-word-service: no banned tokens matched (v3.2)',
});
const signed = meta.images.find(i => i.type === 'signed');
console.log(signed.url, fromCache ? '(cache)' : '(generated)');
if (fromCache && matches.length) console.log('alternatives', matches.map(m => m.url));
```

## Minimal Python Client

```python
import os, time, base64, requests

API = "https://imgen.api.efficientstack.com/api/v2"
TOKEN = os.environ["IMGEN_API_KEY"]
HEADERS = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}

def call(body):
    return requests.post(API, json=body, headers=HEADERS, timeout=30).json()

def generate(prompt, *, model="eros", style="", use_cache=False, ruleset="", precompliance=""):
    job = call({
        "action": "generate", "prompt": prompt, "model": model, "style": style,
        "use_cache": use_cache, "compliance_ruleset": ruleset, "precompliance": precompliance,
    })
    if "error" in job:
        raise RuntimeError(job["error"])

    for _ in range(120):
        time.sleep(3)
        s = call({"action": "status", "id": job["id"], "prompt": job["prompt"], "model": job["model"]})
        if s["status"] in ("IN_QUEUE", "IN_PROGRESS"):
            continue
        if s["status"] == "COMPLETED":
            if s.get("compliance") == "unsafe":
                blocked = base64.b64decode(s["unsafe_url"]).decode() if s.get("unsafe_url") else None
                raise RuntimeError(f"{s.get('error')} (blocked_url={blocked})")
            return s["meta"], bool(s.get("cache")), s.get("matches", [])
        raise RuntimeError(s.get("error", f"Failed: {s['status']}"))
    raise TimeoutError("Polling timed out")

meta, from_cache, matches = generate(
    "@aya black lingerie, warm lighting",
    style="professional",
    use_cache=True,
    precompliance="ban-word-service: no banned tokens matched (v3.2)",
)
signed = next(i for i in meta["images"] if i["type"] == "signed")
print(signed["url"], "(cache)" if from_cache else "(generated)")
if from_cache and matches:
    print("alternatives", [m["url"] for m in matches])
```
