REST API ยท v1 ยท stable

BizImages API documentation

Generate brand-coherent AI photography from a single description. Submit a job, poll until done, integrate the photos. Every photo comes with full SEO metadata, alt text and a public detail page already wired into the catalog.

Currently using the demo key. Generate your own โ†’

Quickstart โ€” 60 seconds

Submit a job, poll until done, grab the photos.

## 1. Submit the job
curl -X POST https://www.fonori.com/api/v1/jobs \
  -H "X-API-Key: live_f7a15e86f2c7a69b13331664093156fd51f19bcdd72693d8" \
  -H "Content-Type: application/json" \
  -d '{
    "description": "Sweet Moment, a boutique pastry shop in Brooklyn. Warm wooden interior, handmade cakes.",
    "count": 4,
    "style": "Photorealistic",
    "purpose": "Company website",
    "size": "1536x1024",
    "subjects": ["Interior","Macro details","Candid moments"]
  }'

## โ†’ 202 Accepted, returns { job_id, poll_url, ... }

## 2. Poll until done
curl -H "X-API-Key: live_f7a15e86f2c7a69b13331664093156fd51f19bcdd72693d8" \
  https://www.fonori.com/api/v1/jobs/{job_id}

## status: queued โ†’ running โ†’ done | failed
<?php
$apiKey = 'live_f7a15e86f2c7a69b13331664093156fd51f19bcdd72693d8';
$base   = 'https://www.fonori.com/api/v1';

// 1. Submit
$ch = curl_init("$base/jobs");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "X-API-Key: $apiKey",
        "Content-Type: application/json",
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'description' => 'Boutique law firm in Berlin, minimalist interior',
        'count'       => 4,
        'style'       => 'Editorial',
        'purpose'     => 'Company website',
        'size'        => '1536x1024',
        'subjects'    => ['Interior', 'Portraits'],
    ]),
]);
$job = json_decode(curl_exec($ch), true);
curl_close($ch);
$jobId = $job['job_id'];

// 2. Poll until done (with backoff)
do {
    sleep(5);
    $ch = curl_init("$base/jobs/$jobId");
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => ["X-API-Key: $apiKey"],
    ]);
    $state = json_decode(curl_exec($ch), true);
    curl_close($ch);
    echo "Status: {$state['status']} ({$state['progress']}%)\n";
} while (in_array($state['status'], ['queued','running']));

if ($state['status'] === 'done') {
    foreach ($state['photos'] as $p) {
        echo $p['title'] . ' โ€” ' . $p['url'] . "\n";
        file_put_contents("img-{$p['id']}.png", file_get_contents($p['url']));
    }
} else {
    fwrite(STDERR, "Job failed: " . $state['error'] . "\n");
}
const API = 'https://www.fonori.com/api/v1';
const KEY = 'live_f7a15e86f2c7a69b13331664093156fd51f19bcdd72693d8';
const H   = { 'X-API-Key': KEY, 'Content-Type': 'application/json' };

// 1. Submit
const submit = await fetch(`${API}/jobs`, {
  method: 'POST', headers: H,
  body: JSON.stringify({
    description: 'Yoga studio with bamboo floors and morning light',
    count: 6, style: 'Minimal & clean',
    purpose: 'Social media', size: '1024x1024',
    subjects: ['Interior','Lifestyle / in-use'],
  }),
});
const { job_id } = await submit.json();

// 2. Poll
let state;
do {
  await new Promise(r => setTimeout(r, 5000));
  state = await (await fetch(`${API}/jobs/${job_id}`, { headers: H })).json();
  console.log(state.status, state.progress + '%');
} while (state.status === 'queued' || state.status === 'running');

if (state.status === 'done') {
  state.photos.forEach(p => console.log(p.title, 'โ†’', p.url));
} else {
  throw new Error(state.error);
}
import requests, time

API = "https://www.fonori.com/api/v1"
H   = {"X-API-Key": "live_f7a15e86f2c7a69b13331664093156fd51f19bcdd72693d8"}

# 1. Submit
job = requests.post(f"{API}/jobs", headers=H, json={
    "description": "Specialty coffee roaster, raw concrete, pour-over bar",
    "count": 4, "style": "Cinematic",
    "purpose": "Advertising", "size": "1792x1024",
    "subjects": ["Interior","Macro details","Atmosphere"],
}).json()
job_id = job["job_id"]

# 2. Poll
while True:
    state = requests.get(f"{API}/jobs/{job_id}", headers=H).json()
    print(state["status"], state["progress"], "%")
    if state["status"] in ("done", "failed"):
        break
    time.sleep(5)

if state["status"] == "done":
    for p in state["photos"]:
        with open(f"img-{p['id']}.png", "wb") as f:
            f.write(requests.get(p["url"]).content)
        print("saved", p["title"])
else:
    raise SystemExit(state["error"])

Authentication

All write endpoints require an API key. Pass it via either header โ€” both are equivalent:

X-API-Key: live_f7a15e86f2c7a69b13331664093156fd51f19bcdd72693d8# or
Authorization: Bearer live_f7a15e86f2c7a69b13331664093156fd51f19bcdd72693d8

Demo key for this local install: live_f7a15e86f2c7a69b13331664093156fd51f19bcdd72693d8 โ€” change it in config.php before exposing to the public internet.

Read endpoints (GET /photos, GET /photos/{slug}, GET /health) are public. Write endpoints (POST /jobs, all DELETE) require a key.

Flow overview

  1. POST /jobs โ€” describe the company. Returns a job_id in < 200 ms (202 Accepted).
  2. Server runs the work in the background for up to 600 s โ€” generates prompts, renders each image (~10โ€“25 s each), generates SEO metadata, optionally uploads to S3, persists to the catalog.
  3. GET /jobs/{id} โ€” poll every 3โ€“10 s. Status moves queued โ†’ running โ†’ done (or failed). The photos array is populated incrementally so you can stream-render.
  4. Use the photos โ€” each entry has a CDN-ready url, alt text, title, description, keywords, and a page_url linking to the public catalog page (already SEO-optimized with structured data).

Choosing the right endpoint

The API has two layers. Pick the most specific one โ€” it'll usually give you a better image with less prompt engineering on your side.

Your use-caseRecommended endpointWhy
Product photo for ecommercePOST /niche-generate with niche=product-photo
or POST /products
Source-image-aware, structured fields (category, marketplace, mood), safety rules tuned for product shots.
Passport / ID / visa portraitPOST /niche-generate with niche=passport-photoCountry-aware sizing, attire and background presets, fake-document blocklist.
LinkedIn / professional headshotPOST /niche-generate with niche=linkedin-headshotProfession-aware attire + background, identity-preserving prompt.
Virtual staging / empty room โ†’ furnishedPOST /niche-generate with niche=virtual-stagingLocks architecture; only adds furniture.
Body-transformation visualizationPOST /niche-generate with niche=body-transformationRealistic body changes, healthy proportions, ED-safety rules.
Old photo restoration / colorizationPOST /niche-generate with niche=photo-restorationIdentity-faithful restoration prompt.
Dog breed mix visualizerPOST /niche-generate with niche=dog-breed-mixTwo-parent selects + mix-ratio slider, animal-safety rules.
Classic car / vintage motorcycle posterPOST /niche-generate with niche=classic-car or niche=motorcycle-vintageEra + archetype + environment fields, manufacturer-logo blocklist.
Historical city reconstructionPOST /niche-generate with niche=historical-cityCity + period + scene fields, propaganda-symbol blocklist.
Interior design conceptPOST /niche-generate with niche=interior-designRoom + style + palette + lighting fields.
Paid social / display ad creativePOST /niche-generate with niche=ad-creativePlatform + industry + campaign + mood fields.
Anything else / free-form brand setPOST /auto-generate (one-shot, system picks everything)
or generic POST /jobs (full control)
Generic pipeline. Use only when no specialized niche fits.

All specialized endpoints return photos with the same shape as /photos, so existing integrations keep working โ€” they just get higher-quality results.

GET /niche-generate

List every specialized tool available right now. Use this once to discover what's possible, then drive your integration off the slugs.

curl https://www.fonori.com/api/v1/niche-generate

Response 200

{
  "data": [
    { "slug": "product-photo", "title": "AI Product Photo Generator",
      "tagline": "Upload a product, pick a scene โ€” get marketplace-ready shots.",
      "requires_source_image": true, "count_max": 4,
      "sizes": ["1024x1024","1024x1536","1536x1024"],
      "page_url": "https://www.fonori.com/n/product-photo" },
    { "slug": "passport-photo", "title": "AI Passport & ID Photo Generator", "...": "..." },
    { "slug": "dog-breed-mix",  "title": "AI Dog Breed Mix Visualizer",       "...": "..." }
  ],
  "count": 12
}

GET /niche-generate?slug={slug}

Describe one niche โ€” fields, sizes, prompt template, disclaimer, monetization tier. Use this to drive a dynamic form on your side. The server-side blocked_terms list is hidden from the response.

curl https://www.fonori.com/api/v1/niche-generate?slug=passport-photo

Response 200 (excerpt)

{
  "niche": {
    "slug": "passport-photo",
    "title": "AI Passport & ID Photo Generator",
    "requires_source_image": true,
    "mode": "edit",
    "count_max": 1,
    "default_size": "1024x1024",
    "sizes": { "1024x1024": "Square 1:1", "1024x1536": "Portrait 2:3" },
    "fields": [
      { "name": "country",    "type": "select", "required": true,
        "options": ["United States","United Kingdom","Germany", "..."] },
      { "name": "document",   "type": "select", "required": true,
        "options": ["passport","visa","national ID","driving licence"] },
      { "name": "background", "type": "select", "default": "white",
        "options": ["white","off-white","light grey","light blue"] },
      { "name": "attire",     "type": "select", "default": "plain dark shirt", "...": "..." }
    ],
    "disclaimer": "AI-prepared portrait..."
  }
}

POST /niche-generate

Run a specialized tool. The server validates attributes against the niche's field schema, builds the prompt from the niche's template, runs niche-specific safety, then generates.

Body

{
  "niche":      "passport-photo",          // required, slug from GET /niche-generate
  "attributes": {                          // validated against the niche schema
    "country":  "Czech Republic",
    "document": "passport",
    "background": "white",
    "attire":   "business attire"
  },
  "image_b64":  "iVBORw0KGgo...",          // required if niche.requires_source_image
  "image_url":  "https://...",             // ...or pass a URL instead
  "size":       "1024x1024",               // optional, defaults to niche.default_size
  "count":      1,                         // 1..niche.count_max
  "brand":      "Acme",                    // optional, stamped on each photo
  "client_ref": "sku-12345",               // optional, your own tag for retrieval
  "series_id":  "spring-2026"              // optional, auto-generated if omitted
}

cURL

curl -X POST https://www.fonori.com/api/v1/niche-generate \
  -H "X-API-Key: live_f7a15e86f2c7a69b13331664093156fd51f19bcdd72693d8" \
  -H "Content-Type: application/json" \
  -d '{
    "niche":"product-photo",
    "image_url":"https://example.com/my-perfume.png",
    "attributes":{"category":"perfume","background":"marble surface","mood":"luxury editorial","platform":"Shopify"},
    "client_ref":"sku-12345",
    "count":2
  }'

Response 201

{
  "data": [ /* photo objects, same shape as /photos */ ],
  "niche": "product-photo",
  "series_id": "auto-uuid-if-not-supplied",
  "prompt": "Final assembled prompt sent to the image model",
  "disclaimer": "Output is generated and may differ from the source product...",
  "count": 2
}

POST /products

Direct product-photo remix โ€” convenience endpoint for ecommerce flows. Equivalent to POST /niche-generate with niche=product-photo but with a free-form prompt instead of structured attributes.

{
  "prompt":      "Luxury perfume bottle on marble bathroom counter, soft cinematic light",
  "image_b64":   "...",                    // or image_url
  "image_url":   "https://...",
  "brand":       "Acme",                   // optional
  "series_id":   "spring-2026",            // optional, auto-uuid if omitted
  "size":        "1024x1024",
  "count":       1,                        // 1..4
  "description": "Free-text addon copy",   // optional
  "client_ref":  "sku-12345"               // optional
}

GET /products

List product photos (everything with category=ecommerce). Filter by brand, series_id, client_ref or full-text q.

curl "https://www.fonori.com/api/v1/products?client_ref=sku-12345&limit=20"

GET /random-source?niche={slug}

Get a generic AI-generated source image for a given niche โ€” useful for demos, tests, and onboarding flows where the user hasn't uploaded anything yet. Returns a stable URL you can pass as image_url in a follow-up POST /niche-generate.

curl https://www.fonori.com/api/random-source?niche=virtual-staging

Response 200

{ "url": "https://www.fonori.com/storage/photos/rndsrc-virtual-staging-abc123.png", "slug": "rndsrc-virtual-staging-abc123" }

Endpoint maintains a small per-niche pool of cached sources โ€” first hit generates, subsequent hits reuse.

POST /auto-generate

Autonomous endpoint โ€” caller sends one free-text description and a count, system decides everything else (style, purpose, subjects, prompts). Async like POST /jobs; returns a series_id for later retrieval.

{
  "description": "Sweet Moment, a boutique pastry shop in Brooklyn...",
  "count":       4,                        // 1..api_max_count
  "size":        "1536x1024",              // optional
  "client_ref":  "campaign-spring"         // optional
}

Response 202

{
  "series_id":  "<uuid>",
  "job_id":     "<uuid>",                 // same value as series_id
  "status":     "queued",
  "poll_url":   "https://www.fonori.com/api/v1/jobs/<uuid>",
  "photos_url": "https://www.fonori.com/api/v1/myimages?series_id=<uuid>",
  "estimated_seconds": 56
}

GET /myimages

List photos you generated through your API key. Pairs perfectly with client_ref so you can fetch the images attached to one of your own items (SKU, order, product, anything).

curl -H "X-API-Key: live_f7a15e86f2c7a69b13331664093156fd51f19bcdd72693d8" \
  "https://www.fonori.com/api/v1/myimages?client_ref=sku-12345"

Query parameters

  • series_id โ€” restrict to one generation batch
  • client_ref โ€” your own tag attached at create time
  • limit โ€” default 50, max 200
  • offset โ€” pagination

GET /job-status?id={uuid}

Lightweight unauthenticated read of a job's state (UUIDs are unguessable). Use this to poll instead of /jobs/{id} if you don't have an API key in the browser.

{
  "id": "<uuid>",
  "status": "running",       // queued | running | done | failed
  "progress": 45,
  "stage": "rendering 1 of 4",
  "elapsed_s": 28,
  "heartbeat_age": 3,
  "stuck": false,            // true if heartbeat > 90s old while running
  "error": null,
  "photos": [ /* present once status="done" */ ]
}

GET /job-run?id={uuid}

Runs the worker logic synchronously inside the HTTP request โ€” used by the niche pages so the user's browser drives generation when no cron is available. Returns once the job is done (or already was).

Safe to call concurrently โ€” the worker's internal "not queued" guard exits duplicate runners immediately.

GET /health

Liveness check + library stats. No auth.

curl https://www.fonori.com/api/v1/health

Response 200

{
  "status": "ok",
  "service": "BizImages API",
  "version": "v1",
  "time": "2026-05-09T10:30:00+00:00",
  "stats": { "photos": 124, "jobs": 38 }
}

POST /jobs

Submit an image-generation job. Returns immediately with a job_id.

Request body

FieldTypeRequiredDescription
descriptionstringโœ“Company description, โ‰ฅ 4 chars. The richer, the better the prompts.
countintโ€”Photos to generate. 1โ€“12 (default 4).
sizestringโ€”One of 1024x1024, 1024x1536, 1536x1024, 1024x1792, 1792x1024. Default 1024x1024.
stylestringโ€”Visual style label, e.g. Photorealistic, Cinematic, Editorial, Minimal & clean, Vibrant & bold, Illustration, 3D render, Vintage film.
purposestringโ€”Use case label (free text). e.g. Company website, Social media, Advertising.
subjectsstring[]โ€”Subject hints โ€” e.g. ["Interior","Exterior","Candid moments","Portraits","Products","Macro details","Atmosphere","Lifestyle / in-use"].
promptsstring[]โ€”Optional. If provided, the server skips prompt generation and renders these directly.

Response 202 Accepted

{
  "job_id": "f9b3c2e6-1e4a-4b21-9c3f-a1d8e9c12345",
  "status": "queued",
  "progress": 0,
  "poll_url": "https://www.fonori.com/api/v1/jobs/f9b3c2e6-1e4a-4b21-9c3f-a1d8e9c12345",
  "estimated_seconds": 56,
  "created_at": "2026-05-09T10:30:00+00:00"
}

Response 422 โ€” validation error

{
  "error": {
    "type": "unprocessable",
    "message": "`count` must be between 1 and 12.",
    "status": 422
  }
}

GET /jobs/{id}

Get current job state. Photos are filled in incrementally โ€” you can render them as they appear.

curl -H "X-API-Key: live_f7a15e86f2c7a69b13331664093156fd51f19bcdd72693d8" https://www.fonori.com/api/v1/jobs/{job_id}

Response while running

{
  "job_id": "f9b3c2e6-...",
  "status": "running",
  "progress": 55,
  "params": { "description": "...", "count": 4, "size": "1536x1024", ... },
  "prompts": ["A warm pastry shop interior at golden hour, ...", "..."],
  "photos": [
    {
      "id": 421, "slug": "warm-pastry-shop-interior-9a3b1c",
      "url": "http://bizimages.test/storage/photos/warm-pastry-shop-interior-9a3b1c.png",
      "page_url": "http://bizimages.test/photo.php?slug=warm-pastry-shop-interior-9a3b1c",
      "title": "Warm pastry shop interior at golden hour",
      "alt_text": "Boutique pastry shop with handmade cakes and warm wooden interior...",
      "description": "Boutique-style brand photography...",
      "keywords": ["pastry shop","interior","brooklyn","cozy","handmade"],
      "width": 1536, "height": 1024,
      "prompt": "A warm pastry shop interior at golden hour, ..."
    }
  ],
  "photo_count": 2,
  "error": null,
  "created_at": "2026-05-09T10:30:00+00:00",
  "started_at": "2026-05-09T10:30:01+00:00",
  "completed_at": null
}

Response when done

{
  "job_id": "f9b3c2e6-...",
  "status": "done",
  "progress": 100,
  "photos": [ /* 4 photos */ ],
  "completed_at": "2026-05-09T10:31:08+00:00"
}

Response when failed

{
  "job_id": "f9b3c2e6-...",
  "status": "failed",
  "error": "image #2 failed (429): rate limit exceeded",
  "completed_at": "2026-05-09T10:30:42+00:00"
}

GET /jobs

List the 50 most recent jobs (debugging / dashboards).

curl -H "X-API-Key: live_f7a15e86f2c7a69b13331664093156fd51f19bcdd72693d8" https://www.fonori.com/api/v1/jobs

DELETE /jobs/{id}

Remove a job record. Does not delete the photos it produced.

GET /photos

Browse the public catalog. No auth required.

Query params

qstringFree-text search across title, description, keywords.
stylestringExact-match style filter.
limitintPage size 1โ€“100, default 20.
offsetintSkip N items.
formatstringjson (default) or xml.
curl "https://www.fonori.com/api/v1/photos?q=cafe&limit=10"

Response

{
  "data": [ { "id": 421, "slug": "...", "title": "...", "url": "...", ... }, ... ],
  "total": 42,
  "limit": 10,
  "offset": 0,
  "has_more": true
}

GET /photos/{slug}

Single photo with full metadata. Public.

curl https://www.fonori.com/api/v1/photos/warm-pastry-shop-interior-9a3b1c

DELETE /photos/{slug}

Permanently delete from disk, S3 and the catalog. Requires API key.

Sizes & ratios

SizeRatioBest for
1024x10241:1Social profiles, grids
1024x15362:3Stories, mobile hero
1536x10243:2Web hero, banners
1792x102416:9Cinematic, video stills
1024x17929:16Reels, vertical mobile

Style presets

Use any free-text style label; the prompt model will adapt. These are tested presets:

๐Ÿ“ท Photorealistic๐ŸŽฌ Cinematic๐Ÿ“ฐ Editorial โฌœ Minimal & clean๐ŸŒˆ Vibrant & bold๐ŸŽจ Illustration ๐ŸงŠ 3D render๐Ÿ“ผ Vintage film

Error format

All errors share this shape:

{
  "error": {
    "type": "unprocessable",
    "message": "Human-readable reason.",
    "status": 422
  }
}
HTTPtypeWhen
400bad_requestMalformed JSON or missing required body.
401unauthorizedMissing or invalid API key.
404not_foundJob or photo doesn't exist.
405method_not_allowedWrong HTTP verb. Allow: header lists valid ones.
422unprocessableValidation failed (count out of range, bad size, etc.).
502upstream_errorAzure/OpenAI returned an error.

XML responses

Append ?format=xml or send Accept: application/xml:

curl "https://www.fonori.com/api/v1/photos?limit=2&format=xml"
<?xml version="1.0" encoding="UTF-8"?>
<response>
  <data>
    <item>
      <id>421</id>
      <slug>warm-pastry-shop-interior-9a3b1c</slug>
      <title>Warm pastry shop interior at golden hour</title>
      <url>http://bizimages.test/storage/photos/...</url>
      <keywords>
        <item>pastry shop</item>
        <item>interior</item>
      </keywords>
    </item>
  </data>
  <total>42</total>
</response>

๐Ÿค– For agentic coders

Drop this into your agent's context โ€” it has every URL, header and field needed to integrate end-to-end without asking questions.

You are integrating the BizImages API.

BASE URL:  https://www.fonori.com/api/v1API KEY:   live_f7a15e86f2c7a69b13331664093156fd51f19bcdd72693d8 (header: X-API-Key, or Authorization: Bearer)
DOCS:      https://www.fonori.com/docs
PLAYGROUND:https://www.fonori.com/playground

FLOW (async):
  1. POST {BASE}/jobs with JSON {description, count, size, style, purpose, subjects[]}.
     Returns 202 with {job_id, poll_url, estimated_seconds}.
  2. Poll GET {BASE}/jobs/{job_id} every 5 s until status == "done" or "failed".
     `photos` array populates incrementally โ€” render as they arrive.
  3. Each photo: {id, slug, url, page_url, title, alt_text, description, keywords[], width, height, prompt}.
     Use `url` as , `alt_text` as alt, `title` as figcaption, `description` as longer caption.
     `page_url` links to a public SEO landing page already provided.
  4. On status == "failed", read `error` and surface to the user.

VALIDATION:
  - description: required, >= 4 chars, ideally 50-500.
  - count: 1..12.
  - size: one of 1024x1024, 1024x1536, 1536x1024, 1024x1792, 1792x1024.
  - style: free-text but stick to known presets for best output: Photorealistic, Cinematic,
    Editorial, Minimal & clean, Vibrant & bold, Illustration, 3D render, Vintage film.
  - subjects: any of: Interior, Exterior, Candid moments, Portraits, Products, Workspace & tools,
    Customer interaction, Macro details, Atmosphere, Lifestyle / in-use.

ERROR SHAPE: {error: {type, message, status}}. Always check HTTP >= 400.

RATE: keep concurrent jobs <= 4. Polling: 5 s interval is friendly; back off to 10 s after 60 s.

QUICK CURL:
  curl -X POST https://www.fonori.com/api/v1/jobs \
    -H "X-API-Key: live_f7a15e86f2c7a69b13331664093156fd51f19bcdd72693d8" -H "Content-Type: application/json" \
    -d '{"description":"...","count":4,"size":"1536x1024","style":"Photorealistic"}'

CATALOG SEARCH (no auth):
  GET {BASE}/photos?q=cafe&limit=20
  GET {BASE}/photos/{slug}

DELETE (auth required):
  DELETE {BASE}/photos/{slug}
  DELETE {BASE}/jobs/{id}

ALTERNATIVE FORMATS:
  ?format=xml or Accept: application/xml on any GET endpoint.

DO NOT:
  - Hard-code a single image URL โ€” always fetch via API.
  - Submit the same description twice in a tight loop; cache the result.
  - Skip the alt_text โ€” it is human-quality and the legal/SEO basis for using AI imagery.