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

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):

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: services deploy phase in gipity.yaml instead of only via project_settings. The definitions go under service_definitions: (not services:):

- name: services
  type: services
  service_definitions:
    - service: image
      billing_mode: owner_pays   # you pay; no visitor login needed

See deploy for the full services-phase reference and app-llm for billing modes.

Providers & Models

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):

{ "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

Request Format (POST /image)

{
  "prompt": "A sunset over mountains",
  "provider": "openai",
  "model": "gpt-image-2",
  "size": "1024x1024",
  "quality": "auto"
}

Fields:

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 bodyurl, 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