Integrating Image Generation APIs

AIAPIBackendDesign
Share on LinkedIn

Your app lets users generate product mockups from text descriptions. The first integration attempt pipes raw user input to an API and displays whatever returns—including occasional policy violations and $0.08 images that users regenerate five times before accepting one. Production image generation needs prompt templating, safety gates, async job handling, and storage that survives API version changes.

Provider comparison

Provider Model Strengths Typical cost
OpenAI DALL·E 3 Prompt adherence, safety ~$0.04–0.12/image
Stability AI SDXL, SD3 Style control, self-host option ~$0.002–0.03/image
Black Forest Labs Flux Photorealism, text in images Varies by host
Replicate / Fal.ai Many Pay-per-second GPU, no infra Usage-based

Abstract behind an internal interface so you can swap providers without rewriting UI code.

Async job pattern

Image generation takes 3–30 seconds. Don't block HTTP requests.

# POST /api/images/generate
async def create_generation(request: GenerateRequest):
    job_id = str(uuid4())
    await redis.set(f"job:{job_id}", json.dumps({"status": "pending"}))
    await queue.enqueue("generate_image", job_id, request.dict())
    return {"job_id": job_id}

# Worker
async def generate_image(job_id: str, params: dict):
    await redis.set(f"job:{job_id}", json.dumps({"status": "processing"}))
    try:
        result = await openai_client.images.generate(
            model="dall-e-3",
            prompt=build_prompt(params),
            size="1024x1024",
            quality="standard",
            n=1,
        )
        url = result.data[0].url
        stored = await upload_to_s3(await fetch_bytes(url))
        await redis.set(f"job:{job_id}", json.dumps({
            "status": "complete",
            "url": stored,
        }))
    except Exception as e:
        await redis.set(f"job:{job_id}", json.dumps({
            "status": "failed",
            "error": str(e),
        }))

Poll GET /api/images/jobs/{job_id} or push via WebSocket when complete.

Prompt templating

Never pass raw user text directly. Wrap it:

TEMPLATE = """Product photography of {user_description}.
Style: clean white background, soft studio lighting, 45-degree angle.
No text, no watermarks, no people unless specified."""

def build_prompt(params: dict) -> str:
    cleaned = sanitize(params["description"])  # strip injection attempts
    return TEMPLATE.format(user_description=cleaned[:500])

Negative prompts (for Stable Diffusion) suppress common artifacts:

blurry, distorted, low quality, watermark, text overlay, cropped

Safety and abuse prevention

BLOCKED_TERMS = load_blocklist("blocked_terms.txt")

def sanitize(text: str) -> str:
    lower = text.lower()
    for term in BLOCKED_TERMS:
        if term in lower:
            raise PromptRejected(f"blocked content")
    return text.strip()

Add per-user quotas: 10 generations/day free, paid tiers above. Track spend per user_id and alert when daily cost exceeds threshold.

Storage and CDN

async def upload_to_s3(image_bytes: bytes, job_id: str) -> str:
    key = f"generations/{job_id}.webp"
    s3.put_object(
        Bucket="assets",
        Key=key,
        Body=image_bytes,
        ContentType="image/webp",
        Metadata={"job_id": job_id},
    )
    return f"https://cdn.example.com/{key}"

Convert PNG to WebP on ingest—60% smaller with no visible loss. Set Cache-Control: immutable, max-age=31536000 since generation IDs are unique.

Self-hosted Stable Diffusion

When API costs exceed ~$500/month at your volume, a single A10G GPU on RunPod handles 15–20 images/minute with SDXL:

import requests

response = requests.post("http://gpu-worker:7860/sdapi/v1/txt2img", json={
    "prompt": prompt,
    "negative_prompt": negative,
    "steps": 25,
    "width": 1024,
    "height": 1024,
    "cfg_scale": 7,
    "seed": seed,
})
image_b64 = response.json()["images"][0]

Pin model versions in your worker container. Upgrading checkpoints changes output style—communicate that to users.

Caching identical requests

Hash (prompt, size, model, seed) and return cached URL if exists. Users re-clicking "generate" with unchanged inputs shouldn't burn credits.

API selection guide

Provider Model Best for Cost/image
OpenAI DALL·E 3 High quality, prompt adherence ~$0.04–0.12
Stability AI SDXL/SD3 Customization, ControlNet ~$0.002–0.01
Replicate Various Experimentation, fine-tunes ~$0.001–0.005
Self-hosted SDXL High volume (>10k/day) GPU cost only
# OpenAI DALL·E 3
response = client.images.generate(
    model="dall-e-3",
    prompt=prompt,
    size="1024x1024",
    quality="standard",  # or "hd" for 2× cost
    n=1,
)

# Stability AI SDXL
response = requests.post(
    "https://api.stability.ai/v2beta/stable-image/generate/sd3",
    headers={"Authorization": f"Bearer {STABILITY_KEY}"},
    files={"prompt": (None, prompt), "output_format": (None, "webp")},
)

OpenAI for quality-critical generation. Stability/Replicate for volume and customization. Self-hosted when API costs exceed ~$500/month.

Content moderation pipeline

Filter generated images before delivery:

async def generate_and_moderate(prompt: str, user_id: str) -> str:
    # Pre-generation: filter prompt
    if await moderation_api.check(prompt).flagged:
        raise ContentPolicyViolation("Prompt violates content policy")

    image_url = await generate_image(prompt)

    # Post-generation: scan image
    if await vision_moderation.check(image_url).nsfw_score > 0.8:
        await audit_log.record(user_id, prompt, "blocked_nsfw")
        raise ContentPolicyViolation("Generated image blocked")

    return image_url

Moderate both prompt and output. Log blocked generations for policy review and user appeal process.

Generation metadata for audit

Store full generation context for compliance and debugging:

{
  "generation_id": "gen_abc123",
  "user_id": "user_xyz",
  "prompt": "A modern office building at sunset",
  "negative_prompt": "blurry, watermark",
  "model": "stabilityai/sdxl-base-1.0",
  "model_version": "1.0.0",
  "seed": 42,
  "steps": 25,
  "guidance_scale": 7.5,
  "width": 1024,
  "height": 1024,
  "generated_at": "2024-12-27T10:00:00Z",
  "cdn_url": "https://cdn.example.com/gen_abc123.webp"
}

Required for copyright disputes, content moderation appeals, and reproducing specific outputs.

Failure modes

Production checklist

Resources

Frequently asked questions

How do I prevent users from generating inappropriate images?

Layer defenses: provider safety filters, blocklists on prompts, output classifiers (NSFW detection), rate limits per user, and human review queues for flagged generations. Log prompt hashes, not raw prompts, if privacy matters.

Should I store generated images or regenerate on demand?

Store images in object storage (S3, GCS) with the prompt and seed in metadata. Regeneration with the same seed is not guaranteed across API versions. Storage costs pennies per image; regeneration costs API credits and adds latency.

What resolution should I request from image APIs?

1024×1024 is the sweet spot for most providers—good quality, moderate cost. Generate at target size rather than upscaling when possible. Offer 512×512 previews before full-resolution generation to cut spend.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →