Imgen API v2

Introduction

The Imgen API provides AI-powered image generation with multiple diffusion models, built-in character LoRA support, prompt enhancement, content compliance checking, content credentials, and watermarking. All prompt parsing, workflow construction, and post-processing are handled server-side — clients submit raw prompts and receive signed images hosted on S3.

Building with an LLM agent? A condensed LLM friendly reference is served as plain Markdown at /llm-docs.

Base URL

All endpoints accept POST requests with a Content-Type: application/json body containing an action field.

POST https://imgen.api.efficientstack.com/api/v2
Content-Type: application/json
Authorization: Bearer <API-KEY>

{
  "action": "generate",
  ...
}

Authentication

All requests must include a Bearer token in the Authorization header:

Authorization: Bearer <API-KEY>

Requests without a valid, enabled key receive a 401 Unauthorized response.

Prompt Syntax

The API uses a unified raw prompt format. The server parses the following tokens:

TokenSyntaxExampleDescription
Character@name@ayaActivates a character LoRA. One per prompt.
Negative-term-red dressComma-separated term prefixed with - is moved to the negative prompt.
Positiveeverything elseblack lingerie, on bedDescriptive prompt text.

Example

@aya black lingerie, sitting on bed, looking_at_viewer, warm lighting, -red dress, -ugly hands

Parsed as: character=aya, positive=black lingerie, sitting on bed, looking_at_viewer, warm lighting, negative=red dress, ugly hands.

Endpoints

ActionDescription
modelsList available characters, models, and style presets
generateSubmit an image generation job
statusPoll a generation job for completion
optimizeEnhance a prompt with LLM
randomGet a random prompt

POST models

Returns available characters, models, and styles. No parameters beyond action.

{ "action": "models" }

Response

{
  "characters": ["aaliyah", "aya", "bella", "..."],
  "models": {
    "eros": {
      "label": "Eros",
      "default_style": "professional",
      "styles": {
        "professional": { "label": "Professional" },
        "boudoir": { "label": "Boudoir" },
        "..."
      }
    }
  }
}

POST generate

Submit an image generation job. Returns a job ID for polling.

Request Parameters

ParameterTypeRequiredDefaultDescription
actionstringyes"generate"
promptstringyesRaw prompt. Supports @character and -negative.
modelstringno"eros"Diffusion model key.
negativestringno""Additional negatives.
optimizebooleannofalseLLM-enhance the prompt before generation.
use_cachebooleannofalseTry semantic cache before generating. See Cache Lookup.
cache_onlybooleannofalseOnly meaningful when use_cache is true. If the cache lookup misses, return 404 instead of falling back to a normal generation.
cache_forcebooleannofalseOnly meaningful when use_cache is true. Bypass the 0.95 similarity threshold and accept any match (strict model/style/character equality is still enforced).
cache_pickintegerno5Only meaningful when use_cache is true. Size of the top-N pool from which a cached image is randomly selected. Clamped to [1, 10].
widthintegerno1344Image width. Must be a valid pair.
heightintegerno768Image height. Must be a valid pair.
stylestringnomodel defaultStyle preset key for the selected model.
formatstringno"jpeg"jpeg, png, webp, avif.
qualityintegerno95Compression quality (1–100).
losslessbooleannofalseLossless compression (WebP/AVIF).
compliance_rulesetstringno"default"ACS compliance ruleset to apply for this generation. See Compliance & Rulesets.
precompliancestringno""Output of an upstream pre-compliance screening system (e.g. a keyword-based Ban Word Service). Forwarded to ACS as supplementary, non-authoritative context during the image review. Max 2000 characters (excess is truncated). See Pre-compliance Signal.
wm_imagestringno""Watermark as base64 data URI.
wm_positionstringno"bottom-right"Watermark anchor.
wm_scaleintegerno5Watermark scale (1–100).
wm_transparencyintegerno100Watermark opacity (0–100).
wm_rotationintegerno0Rotation degrees (-360–360).
wm_padding_xintegerno10Horizontal padding (0–500).
wm_padding_yintegerno10Vertical padding (0–500).
cs_authorstringno""Override C2PA author.
cs_titlestringno""Override C2PA title.
cs_descriptionstringno""Override C2PA description.
cs_organizationstringno""Override C2PA organization.
cs_vendorstringno""Override C2PA vendor.

Valid Dimensions

1024×1024 (1:1), 1344×768 (16:9), 768×1344 (9:16), 1216×832 (3:2), 832×1216 (2:3), 1152×896 (4:3), 896×1152 (3:4). Invalid pairs fall back to 1344×768.

Response

{
  "id": "run_abc123xyz",
  "prompt": "@aya enhanced prompt text, -red dress",
  "model": "eros",
  "format": "jpeg",
  "gen_id": "a1b2c3d4e5f6g7h8i9j0"
}

Response (Cache Hit)

When use_cache is true and a matching cached image is found, the response shape is identical with an additional cache: true field. The polling step still applies and will return immediately.

{
  "id": "cache_a1b2c3d4e5f6g7h8",
  "prompt": "@aya enhanced prompt text, -red dress",
  "model": "eros",
  "format": "jpeg",
  "gen_id": "a1b2c3d4e5f6g7h8i9j0",
  "cache": true
}

Response (Cache Miss with cache_only)

When use_cache and cache_only are both true and the lookup returns no eligible match, the API responds with 404 instead of submitting a generation job.

{ "error": "No cached result found" }

POST status

Poll a generation job. Pass the id, prompt, and model returned by generate.

{
  "action": "status",
  "id": "run_abc123xyz",
  "prompt": "@aya enhanced prompt text, -red dress",
  "model": "eros"
}

Pending

{ "status": "IN_QUEUE" }
{ "status": "IN_PROGRESS" }

Completed (Safe)

{
  "status": "COMPLETED",
  "meta": {
    "id": "run_abc123xyz",
    "gen_id": "a1b2c3d4e5f6g7h8i9j0",
    "time": "2025-03-29T20:02:50Z",
    "prompt": "@aya enhanced prompt, -red dress",
    "positive": "photo of aya, enhanced prompt, quality tags",
    "negative": "base negatives, style negatives, red dress",
    "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": "a4f8e2c1b9d0...",
    "images": [
      { "url": "https://s3...signed.jpg?...", "filename": "genid_fingerprint_00001.jpg", "type": "signed" },
      { "url": "https://s3...thumb.jpg?...", "filename": "genid_fingerprint_00001_thumb.jpg", "type": "thumbnail" }
    ],
    "enable_invisimark": true,
    "enable_c2pa": true,
    "compute_pdq": true,
    "wm_position": "bottom-right",
    "wm_scale": 5,
    "wm_transparency": 100,
    "cs_author": "Author",
    "cs_title": "AI Image",
    "..."
  }
}

Completed (Cache Hit)

For cache-served jobs the same shape is returned, with the additional cache: true field. Compliance is always "safe" (compliance review was performed when the image was originally generated and cached).

{
  "status": "COMPLETED",
  "cache": true,
  "meta": {
    "id": "cache_a1b2c3d4e5f6g7h8",
    "compliance": "safe",
    "images": [
      { "url": "...", "filename": "", "type": "signed" },
      { "url": "...", "filename": "", "type": "thumbnail" }
    ],
    "..."
  }
}

Completed (Blocked)

{
  "status": "COMPLETED",
  "compliance": "unsafe",
  "error": "Content blocked — flagged for: Category Name.",
  "filter_categories": ["id1", "id2"],
  "unsafe_url": "aHR0cHM6Ly9zMy5leGFtcGxlLmNvbS9zaWduZWQuanBnP3NpZz0uLi4="
}
FieldTypeDescription
compliancestringAlways "unsafe" when the response is a block.
errorstringHuman-readable summary of the violations.
filter_categoriesstring[]Array of ACS rule IDs that triggered the block.
unsafe_urlstringBase64-encoded signed S3 URL of the blocked image. Decode with atob() (browser) or base64.b64decode() (Python) to recover the original URL. Provided so moderation tools and trusted clients can still access the asset; not surfaced as a normal images array to discourage casual display.

Failed

{ "status": "FAILED", "error": "Generation failed" }

POST optimize

Enhance a prompt independently of generation.

{
  "action": "optimize",
  "prompt": "@aya black lingerie, on bed, -red dress",
  "model": "eros"
}
{
  "prompt": "@aya elegant black lace lingerie, perched on bed, warm lighting, -red dress"
}

POST random

Returns a random prompt. Omit character for random, set empty "" for none, or specify a name.

{ "action": "random" }
{ "action": "random", "character": "aya" }
{ "action": "random", "character": "" }
{ "prompt": "@aya black lace lingerie, sitting on bed, warm lighting" }

Prompt Construction

Positive Prompt

[trigger], [character_positive], {user prompt}[, style quality tags]

Negative Prompt

{base negatives}[, style negatives][, character negatives][, parsed -terms][, explicit negative]

Cache Lookup

Set use_cache: true on a generate request to opportunistically serve a pre-existing image instead of running a full generation pipeline. The cache is a semantic store: queries are matched on prompt meaning while enforcing strict equality on model, style, and character.

How it works

  1. After prompt parsing and validation, the API queries the semantic cache with the user's prompt plus the resolved model, style, and character.
  2. Candidates must strictly match model, style, and character, and must include a url.
  3. By default, only candidates whose similarity score is above 0.95 are eligible. Pass cache_force: true to disable this floor and accept any returned match.
  4. One of the top cache_pick eligible candidates (default 5, clamped to [1, 10]) is selected at random.
  5. The response uses a cache_ job ID prefix and includes cache: true.
  6. If no eligible candidate exists:
    • With cache_only: false (default), the API silently falls through to a normal generation.
    • With cache_only: true, the API responds with 404 and {"error":"No cached result found"}.

Important

  • Bypasses optimization: when a cache hit is returned, optimize is not applied (the cache is queried with the user's intent prompt).
  • Bypasses compliance: cached images were already reviewed when they were generated, so no new ACS check runs. precompliance is ignored on cache hits.
  • Time-limited: cached results are retrievable for up to one hour after the generate call. Poll promptly.
  • Quality vs. variety trade-off: raising cache_pick increases response variety but may include lower-similarity matches; lowering it (e.g. 1) deterministically returns the single best match.
  • cache_force caution: with cache_force: true, matches with very low semantic similarity may be returned. Use only when you need a fallback image at any cost.

Example

{
  "action": "generate",
  "prompt": "@aya black lingerie, warm lighting",
  "model": "eros",
  "style": "professional",
  "use_cache": true,
  "cache_only": false,
  "cache_force": false,
  "cache_pick": 5
}

Compliance & Rulesets

After generation, every (non-cached) image is automatically reviewed by the ACS compliance service before its URL is released to the client. Reviews happen during status polling — when the underlying job reaches COMPLETED, the API runs the check and either:

  • returns compliance: "safe" with the full meta.images array, or
  • returns compliance: "unsafe" with a base64-encoded unsafe_url (see status).

Selecting a ruleset

ACS rulesets define the active set of categories applied to a review. Pass compliance_ruleset on the generate request to choose a non-default ruleset for that job:

{
  "action": "generate",
  "prompt": "@aya black lingerie, warm lighting",
  "model": "eros",
  "compliance_ruleset": "strict"
}

Rules:

  • If compliance_ruleset is omitted, empty, or whitespace-only, the API sends "default" to ACS.
  • The selected ruleset is persisted on the generation's metadata and reused when status triggers the review — clients do not need to pass it again on poll.
  • Cache-hit jobs ignore compliance_ruleset entirely; cached images are pre-cleared.

Pre-compliance Signal

Many clients run an upstream pre-compliance system on the raw text prompt before submitting it to Imgen. The precompliance parameter on generate lets you forward that upstream system's output to Imgen so it can be included in the ACS review of the resulting image as additional context.

Behavior

  • Non-authoritative: ACS still makes the final compliance decision. The pre-compliance signal is one input among others.
  • Truncated: values longer than 2000 characters are truncated server-side.
  • Cache hits ignore it: cached images were already cleared; no new ACS review runs.

Example

{
  "action": "generate",
  "prompt": "@aya black lingerie, warm lighting",
  "model": "eros",
  "precompliance": "no banned tokens matched; risk_score=0.04"
}
Recommendation Always forward the raw output of your upstream pre-compliance system verbatim — including any score, version, or matched-keyword summary it produces. ACS interprets the wrapped context string, so more detail is better than a boolean.

Content Credentials

All generated images include C2PA manifest signing, invisible watermark embedding, and PDQ fingerprinting. The signed image and thumbnail are delivered as pre-signed S3 URLs in meta.images.

Override C2PA metadata per request: cs_author, cs_title, cs_description, cs_organization, cs_vendor.

Polling Strategy

SettingRecommended
Interval3 seconds
Max polls120 (6 min timeout)
Overlap guardWait for each poll to complete before starting the next

Terminal states: COMPLETED, FAILED, TIMED_OUT, CANCELLED. On COMPLETED, always check compliance before accessing meta.

Error Handling

StatusErrorCause
400Missing actionNo action field
400Prompt is requiredEmpty/missing prompt
400Invalid characterUnknown @name
400Bad IDInvalid job ID
401UnauthorizedMissing or invalid API key
404No cached result founduse_cache + cache_only with no eligible match
502Generation failedUpstream error
502Enhancement failedLLM error

Silent fallbacks: invalid model → first model, invalid dimensions → 1344×768, invalid style → model default, invalid format → jpeg, cache miss/error → normal generation (unless cache_only is set), empty compliance_ruleset"default", cache_pick out of range → clamped to [1, 10], precompliance longer than 2000 chars → truncated.

Code Examples

const API = 'https://imgen.api.efficientstack.com/api/v2';
const TOKEN = 'your-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 generateImage(prompt, opts = {}) {
  const job = await api({
    action: 'generate', prompt, model: opts.model || 'eros',
    optimize: opts.optimize || false,
    use_cache: opts.use_cache || false,
    cache_only: opts.cache_only || false,
    cache_force: opts.cache_force || false,
    cache_pick: opts.cache_pick || 5,
    width: opts.width || 1344, height: opts.height || 768,
    style: opts.style || '', format: opts.format || 'jpeg',
    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 };
    }
    throw new Error(s.error || 'Failed: ' + s.status);
  }
  throw new Error('Timeout');
}

const { meta, fromCache } = await generateImage('@aya black lingerie, warm lighting', {
  style: 'professional',
  use_cache: true, cache_only: false, cache_force: false, cache_pick: 5,
  compliance_ruleset: 'default',
  precompliance: 'ban-word-service v3.2: no banned tokens matched; risk_score=0.04'
});
const signed = meta.images.find(i => i.type === 'signed');
console.log('Image URL:', signed?.url, fromCache ? '(cached)' : '(generated)');
import requests, time, base64

API = "https://imgen.api.efficientstack.com/api/v2"
TOKEN = "your-api-key"

def api(body):
    return requests.post(API, json=body,
        headers={"Authorization": f"Bearer {TOKEN}"}).json()

def generate(prompt, model="eros", style="", fmt="jpeg",
             use_cache=False, cache_only=False, cache_force=False, cache_pick=5,
             ruleset="", precompliance=""):
    job = api({"action":"generate","prompt":prompt,"model":model,"style":style,
               "format":fmt,
               "use_cache":use_cache,"cache_only":cache_only,
               "cache_force":cache_force,"cache_pick":cache_pick,
               "compliance_ruleset":ruleset,
               "precompliance":precompliance})
    if "error" in job: raise Exception(job["error"])
    for _ in range(120):
        time.sleep(3)
        s = api({"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_url = base64.b64decode(s["unsafe_url"]).decode() if s.get("unsafe_url") else None
                raise Exception(f"{s.get('error')} (blocked_url={blocked_url})")
            return s["meta"], bool(s.get("cache"))
        raise Exception(s.get("error", f"Failed: {s['status']}"))
    raise TimeoutError("Polling timed out")

meta, from_cache = generate("@aya black lingerie, warm lighting",
                            style="professional",
                            use_cache=True, cache_only=False,
                            cache_force=False, cache_pick=5,
                            ruleset="default",
                            precompliance="ban-word-service v3.2: no banned tokens matched; risk_score=0.04")
signed = next(i for i in meta["images"] if i["type"] == "signed")
print("URL:", signed["url"], "(cached)" if from_cache else "(generated)")
TOKEN="your-api-key"
BASE="https://imgen.api.efficientstack.com/api/v2"

# List models
curl -s -X POST "$BASE" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"action":"models"}'

# Generate with pre-compliance context forwarded from a Ban Word Service
curl -s -X POST "$BASE" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"action":"generate","prompt":"@aya black lingerie","model":"eros","style":"professional","compliance_ruleset":"default","precompliance":"ban-word-service v3.2: no banned tokens matched"}'

# Poll status (replace JOB_ID)
curl -s -X POST "$BASE" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"action":"status","id":"JOB_ID","prompt":"@aya ...","model":"eros"}'
<?php
$api = 'https://imgen.api.efficientstack.com/api/v2';
$token = 'your-api-key';

function apiCall($api, $token, $body) {
    $ch = curl_init($api);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => json_encode($body),
        CURLOPT_HTTPHEADER => ['Content-Type: application/json', "Authorization: Bearer $token"],
    ]);
    $r = curl_exec($ch); curl_close($ch);
    return json_decode($r, true);
}

$job = apiCall($api, $token, [
    'action'=>'generate','prompt'=>'@aya black lingerie','model'=>'eros',
    'use_cache'=>true,'cache_only'=>false,'cache_force'=>false,'cache_pick'=>5,
    'compliance_ruleset'=>'default',
    'precompliance'=>'ban-word-service v3.2: no banned tokens matched'
]);
for ($i = 0; $i < 120; $i++) {
    sleep(3);
    $s = apiCall($api, $token, ['action'=>'status','id'=>$job['id'],'prompt'=>$job['prompt'],'model'=>$job['model']]);
    if (in_array($s['status'], ['IN_QUEUE','IN_PROGRESS'])) continue;
    if ($s['status'] === 'COMPLETED') {
        if (($s['compliance'] ?? '') === 'unsafe') {
            $blockedUrl = !empty($s['unsafe_url']) ? base64_decode($s['unsafe_url']) : null;
            die($s['error'] . ($blockedUrl ? " (blocked_url=$blockedUrl)" : ''));
        }
        $signed = array_values(array_filter($s['meta']['images'], fn($i) => $i['type']==='signed'))[0];
        $cached = !empty($s['cache']) ? ' (cached)' : '';
        echo "URL: " . $signed['url'] . $cached; break;
    }
    die($s['error'] ?? 'Failed');
}