Image generation is available for every project with no setup needed (user_pays billing by default). Building a no-login app (like a public photo editor)? user_pays bills the signed-in visitor, so anonymous callers get 401 LOGIN_REQUIRED — set image to owner_pays so it works for everyone (see billing below).
Plan availability: Image generation is limited to 3 uses per month on the Free plan, then unlimited on Pro. When a Free-plan owner exceeds the cap, this endpoint returns 403 FORBIDDEN with an upgrade message - handle that response in the app's UI.
Two modes: generate OR edit
- Generate (text → image): send just a
prompt, get a brand-new image. - Edit (image + instruction → image): send
promptand animagesarray of source image(s). The prompt is an edit instruction applied to them — "make it night", "add a hat", "remove the car in the background", "turn this into a watercolor". This is how you build a photo editor: upload → edit → show before/after.
Editing runs on Gemini automatically (its image model does instruction editing natively) — you don't pick a provider or model for it, just pass images. No GPU job, no custom model, no img2img endpoint to stand up. Same { url, credits_used, … } response as generation.
For purely mechanical changes that don't need a model — crops, rotations, simple filters, overlays/compositing — do it client-side with canvas/WebGL (free, instant, no round trip).
Use project_settings to customize (optional):
- Switch billing mode (owner_pays ↔ user_pays)
- Restrict allowed providers
- Set default provider/model
To make the billing choice reproducible (it ships with the app instead of living as out-of-band server state), declare it in a
type: servicesdeploy phase ingipity.yamlinstead of only viaproject_settings. The definitions go underservice_definitions:(notservices:):- name: services type: services service_definitions: - service: image billing_mode: owner_pays # you pay; no visitor login neededSee deploy for the full services-phase reference and app-llm for billing modes.
Providers & Models
- OpenAI:
gpt-image-2 - BFL/Flux:
flux-2-pro, flux-2-flex, flux-2-max, flux-2-klein-9b, flux-2-klein-4b - Gemini/Nano Banana:
gemini-3.1-flash-lite-image, gemini-2.5-flash-image, gemini-3.1-flash-image, gemini-3-pro-image
Quality tiers (recommended over pinning a model)
Instead of a concrete model id, pass a quality tier as model — it resolves to a current provider+model that we keep pointed at the best option, so your app keeps working when the model lineup refreshes (a pinned concrete id 400s the day it's retired). A tier sets the provider for you (overriding provider):
fast— cheapest, fastest; for thumbnails, drafts, high-volume generationstandard— balanced general-purpose quality (the sensible default)high— high fidelity; for hero images and detailed scenesultra— maximum fidelity; slowest and most expensive
{ "prompt": "A sunset over mountains", "model": "high" }
GET /image/models returns the live tier→provider/model map (tiers) alongside the concrete model list. Pin a concrete id only when you specifically need that exact model.
Endpoints
GET /api/<PROJECT_GUID>/services/image/models- list available providers and modelsPOST /api/<PROJECT_GUID>/services/image- generate an image
Request Format (POST /image)
{
"prompt": "A sunset over mountains",
"provider": "openai",
"model": "gpt-image-2",
"size": "1024x1024",
"quality": "auto"
}
Fields:
prompt(required): Image description (generate) or edit instruction (edit), max 4,000 charsimages: Edit mode. Array (1-4) of source images to edit — each{ "data": "<base64>", "media_type": "image/png" }(jpeg/png/gif/webp). When present,promptis an edit instruction applied to these images and the request routes to Gemini (anyprovider/modelyou pass is ignored for edits). Omit for plain generation.provider: "openai", "bfl", or "gemini" (default: bfl)model: A quality tier —fast,standard,high, orultra(recommended; survives model refreshes and sets the provider) — OR a concrete model id (default: provider's default). See Quality tiers above.size: "WxH" format (default: 1024x1024). OpenAI sizes: 1024x1024, 1024x1536, 1536x1024. BFL: any size (rounded to 32px). Gemini: mapped to nearest resolution tierquality: OpenAI gpt-image-2: low/medium/high/autoaspect_ratio: Gemini only. Aspect ratio: 1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3, 4:5, 5:4, 21:9image_size: Gemini only. Resolution tier: 512, 1K, 2K, 4K (default: 1K)seed: BFL only (ignored by openai/gemini). A non-negative integer that makes generation deterministic — reuse one seed across a series of prompts to keep the images visually coherent (e.g. every illustration in one clip). Omit it for fresh randomness each call.
Response Format
{
"url": "https://media.gipity.ai/med_abc12345.png",
"content_type": "image/png",
"revised_prompt": "...",
"model": "gpt-image-2",
"provider": "openai",
"credits_used": 50
}
The url is a permanent public CDN URL. No auth needed to fetch it.
BFL responses also include a seed field — the seed that produced this image (the one you passed, or the random one BFL picked). Capture it and pass it back as seed on later calls to keep a series of images visually coherent.
CLI
For one-off image generation during development (downloads the result to a local file), skip the HTTP call and use gipity generate image. It writes to ./generated.png in the current directory by default - pass -o <path> to land the file directly in your source tree so it deploys:
gipity generate image "a cat wearing a top hat" -o src/assets/images/hat-cat.png
gipity generate image "landscape sunset" --provider gemini --aspect-ratio 16:9 -o src/assets/images/hero.png
gipity generate image "product photo" --provider openai --model gpt-image-2 --size 1536x1024 --quality high -o src/assets/images/product.png
To edit an existing image (the photo-editor flow) from the CLI, pass --input <file> — the prompt becomes an edit instruction and it routes to Gemini automatically (same as the images app-service param). Use this to verify the edit end-to-end without hand-rolling a token + base64 POST:
gipity generate image "make it night time" --input photo.jpg -o after.png
Client Code Example
Never hardcode a.gipity.ai as the base. That host is only correct on prod; an app deployed by any other instance (dev, local, self-hosted) must call the instance it was deployed to. The deploy injects <script src=".../client/v1/gipity.js" data-app data-api-base> into your <head>, so read the app guid and API base straight off that tag — it's always the right instance.
The image service replies with a flat body — url, content_type, model, credits_used at the TOP level (no { data } wrapper). Read res.json().url directly.
// Read the app guid + API base the deploy stamped onto the SDK script tag.
const sdk = document.querySelector('script[data-api-base]');
const apiBase = sdk.dataset.apiBase; // this instance's API — NOT a hardcoded a.gipity.ai
const app = sdk.dataset.app; // your PROJECT_GUID
// 1. Mint an app token (POST, the token is nested under data)
const { data: { token } } = await (await fetch(`${apiBase}/api/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ app }),
})).json();
// 2. Generate an image — fields are at the top level of the response
const res = await fetch(`${apiBase}/api/${app}/services/image`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-App-Token': token },
body: JSON.stringify({ prompt: 'A cat wearing a top hat' }),
});
const { url } = await res.json(); // { url, content_type, model, credits_used }
// 3. Display
const img = document.createElement('img');
img.src = url; // permanent public CDN URL
document.body.appendChild(img);
Editing an uploaded photo (before/after)
A photo editor uploads the user's picture, sends it with an edit instruction, and shows the result next to the original. Read the file the user picked as base64 and pass it as images (reuse apiBase, app, and token from above):
// file is a File from <input type="file">
const dataUrl = await new Promise((r) => {
const fr = new FileReader();
fr.onload = () => r(fr.result);
fr.readAsDataURL(file);
});
const base64 = dataUrl.split(',')[1]; // strip the "data:...;base64," prefix
const media_type = file.type; // e.g. "image/png"
const res = await fetch(`${apiBase}/api/${app}/services/image`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-App-Token': token },
body: JSON.stringify({
prompt: 'make it night time', // the plain-English edit
images: [{ data: base64, media_type }],
}),
});
const { url } = await res.json(); // top-level url of the edited image
// Show it beside the original for a before/after.
Keep inline images reasonable (the request body caps at 10 MB — a base64 photo is ~1.33× its byte size). Downscale a huge camera photo on a canvas before sending, or for very large originals upload via app-files first. From a serverless function it's the same, via the injected image() service:
const edited = await services.image({ prompt: 'add a party hat', images: [{ data, media_type: 'image/jpeg' }] });
Limits
- Rate limit: 600 requests per 5-minute window (per IP)
- Max prompt length: 4,000 chars
- Timeout: 120s
- Standard
RateLimit-*headers included in responses