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
- POST /jobs โ describe the company. Returns a
job_idin < 200 ms (202 Accepted). - 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.
- GET /jobs/{id} โ poll every 3โ10 s. Status moves
queued โ running โ done(orfailed). Thephotosarray is populated incrementally so you can stream-render. - Use the photos โ each entry has a CDN-ready
url, alt text, title, description, keywords, and apage_urllinking 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-case | Recommended endpoint | Why |
|---|---|---|
| Product photo for ecommerce | POST /niche-generate with niche=product-photoor POST /products | Source-image-aware, structured fields (category, marketplace, mood), safety rules tuned for product shots. |
| Passport / ID / visa portrait | POST /niche-generate with niche=passport-photo | Country-aware sizing, attire and background presets, fake-document blocklist. |
| LinkedIn / professional headshot | POST /niche-generate with niche=linkedin-headshot | Profession-aware attire + background, identity-preserving prompt. |
| Virtual staging / empty room โ furnished | POST /niche-generate with niche=virtual-staging | Locks architecture; only adds furniture. |
| Body-transformation visualization | POST /niche-generate with niche=body-transformation | Realistic body changes, healthy proportions, ED-safety rules. |
| Old photo restoration / colorization | POST /niche-generate with niche=photo-restoration | Identity-faithful restoration prompt. |
| Dog breed mix visualizer | POST /niche-generate with niche=dog-breed-mix | Two-parent selects + mix-ratio slider, animal-safety rules. |
| Classic car / vintage motorcycle poster | POST /niche-generate with niche=classic-car or niche=motorcycle-vintage | Era + archetype + environment fields, manufacturer-logo blocklist. |
| Historical city reconstruction | POST /niche-generate with niche=historical-city | City + period + scene fields, propaganda-symbol blocklist. |
| Interior design concept | POST /niche-generate with niche=interior-design | Room + style + palette + lighting fields. |
| Paid social / display ad creative | POST /niche-generate with niche=ad-creative | Platform + industry + campaign + mood fields. |
| Anything else / free-form brand set | POST /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 batchclient_refโ your own tag attached at create timelimitโ default 50, max 200offsetโ 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
| Field | Type | Required | Description |
|---|---|---|---|
description | string | โ | Company description, โฅ 4 chars. The richer, the better the prompts. |
count | int | โ | Photos to generate. 1โ12 (default 4). |
size | string | โ | One of 1024x1024, 1024x1536, 1536x1024, 1024x1792, 1792x1024. Default 1024x1024. |
style | string | โ | Visual style label, e.g. Photorealistic, Cinematic, Editorial, Minimal & clean, Vibrant & bold, Illustration, 3D render, Vintage film. |
purpose | string | โ | Use case label (free text). e.g. Company website, Social media, Advertising. |
subjects | string[] | โ | Subject hints โ e.g. ["Interior","Exterior","Candid moments","Portraits","Products","Macro details","Atmosphere","Lifestyle / in-use"]. |
prompts | string[] | โ | 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
q | string | Free-text search across title, description, keywords. |
style | string | Exact-match style filter. |
limit | int | Page size 1โ100, default 20. |
offset | int | Skip N items. |
format | string | json (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
| Size | Ratio | Best for |
|---|---|---|
1024x1024 | 1:1 | Social profiles, grids |
1024x1536 | 2:3 | Stories, mobile hero |
1536x1024 | 3:2 | Web hero, banners |
1792x1024 | 16:9 | Cinematic, video stills |
1024x1792 | 9:16 | Reels, vertical mobile |
Style presets
Use any free-text style label; the prompt model will adapt. These are tested presets:
Error format
All errors share this shape:
{
"error": {
"type": "unprocessable",
"message": "Human-readable reason.",
"status": 422
}
}
| HTTP | type | When |
|---|---|---|
| 400 | bad_request | Malformed JSON or missing required body. |
| 401 | unauthorized | Missing or invalid API key. |
| 404 | not_found | Job or photo doesn't exist. |
| 405 | method_not_allowed | Wrong HTTP verb. Allow: header lists valid ones. |
| 422 | unprocessable | Validation failed (count out of range, bad size, etc.). |
| 502 | upstream_error | Azure/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.