Guides
Image Generation
Generate and edit images asynchronously: submit a task, poll for completion, and download signed output URLs.
The Felo image API generates and edits images with models from the Felo model catalog. It is asynchronous: the submit request returns a task ID immediately, and you poll the task until it reaches a terminal state.
This page covers the image-specific contract. For the shared task lifecycle, see Async Tasks.
Before you start
- An API key. See Authentication.
- A model whose
model_kindisimage_generation. See Find an image model. - The base URL
https://openapi.felo.ai.
How it differs from the OpenAI Images API
The path is similar to OpenAI's Images API. The response is not:
| OpenAI Images API | Felo Image API | |
|---|---|---|
| Submit response | 200 with the image data | 202 with task_id, status: "queued" |
| When the image exists | In the submit response | After polling reaches succeeded |
| Image payload | b64_json or url | A time-limited signed url |
| Request errors | HTTP error on submit | Usually status: "failed" on the task |
Do not send openai.images.generate() to this endpoint. The SDK expects a synchronous data array and cannot poll a task. Call the endpoint over HTTP and poll the task, or write a small wrapper for both steps.
Submit a task
curl -X POST "https://openapi.felo.ai/api/v1/images/generations" \
-H "Authorization: Bearer ${FELO_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"model": "gemini-3-pro-image",
"prompt": "A studio product photo of a ceramic coffee cup on warm linen, soft morning light",
"resolution": "2K",
"aspect_ratio": "4:3"
}'A successful submit returns HTTP 202:
{
"task_id": "6d1f0c9a-...",
"status": "queued",
"kind": "image",
"outputs": []
}202 means the platform accepted the task. It does not mean the prompt, parameters, or reference images are valid. The upstream call runs after the response, so most request problems appear later as a failed task. The exception is model routing: a chat or video model is rejected at submit.
Poll until the task is terminal
curl "https://openapi.felo.ai/api/v1/images/generations/${TASK_ID}" \
-H "Authorization: Bearer ${FELO_API_KEY}"| Status | Meaning | What to do |
|---|---|---|
queued | Accepted; waiting to start | Keep polling |
running | The model is generating | Keep polling |
succeeded | Terminal; outputs is populated | Download the outputs |
failed | Terminal; read error.message | Fix the request; do not retry unchanged |
interrupted | Terminal; the result could not be recovered | Resubmit the task |
Task failures keep the HTTP status at 200. Check status in the body, not only the HTTP status.
Start with a 2–5 second interval and back off. Most images finish within a minute; 4K output and edits with many reference images take longer. Always set your own deadline.
import os
import time
import requests
BASE = "https://openapi.felo.ai"
HEADERS = {"Authorization": f"Bearer {os.environ['FELO_API_KEY']}"}
def generate_image(prompt: str, **params) -> list[dict]:
response = requests.post(
f"{BASE}/api/v1/images/generations",
headers=HEADERS,
json={"model": "gemini-3-pro-image", "prompt": prompt, **params},
timeout=30,
)
response.raise_for_status()
task = response.json()
deadline = time.monotonic() + 300
delay = 2.0
while time.monotonic() < deadline:
time.sleep(delay)
delay = min(delay * 1.5, 10.0)
poll = requests.get(
f"{BASE}/api/v1/images/generations/{task['task_id']}",
headers=HEADERS,
timeout=30,
)
poll.raise_for_status()
task = poll.json()
if task["status"] == "succeeded":
return task["outputs"]
if task["status"] in {"failed", "interrupted"}:
message = task.get("error", {}).get("message", task["status"])
raise RuntimeError(message)
raise TimeoutError(f"image task {task['task_id']} did not finish")Read and store the outputs
When the task succeeds, outputs contains one entry per generated image:
{
"task_id": "6d1f0c9a-...",
"status": "succeeded",
"kind": "image",
"outputs": [
{
"media_type": "image/png",
"url": "https://openapi.felo.ai/api/v1/media/...?exp=...&sig=..."
}
]
}outputsis empty until the task succeeds.urlis a signed URL. It downloads without an API key, so treat it as a bearer link.- The URL is time-limited. The
expquery parameter is its expiry time. Download the file and store it in your own bucket or filesystem; the task is not permanent storage. - Polling again after the URL expires returns the same expired URL. It does not issue a fresh one.
- The response does not include base64 image data.
for index, output in enumerate(outputs):
image = requests.get(output["url"], timeout=60)
image.raise_for_status()
with open(f"image-{index}.png", "wb") as file:
file.write(image.content)Edit an image
Pass reference images in input_references. The model uses them as context; the prompt still describes the change.
curl -X POST "https://openapi.felo.ai/api/v1/images/generations" \
-H "Authorization: Bearer ${FELO_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"model": "gemini-3-pro-image",
"prompt": "Keep the product and framing. Replace the background with a sunlit marble countertop.",
"resolution": "2K",
"aspect_ratio": "4:3",
"input_references": [
{
"type": "image_url",
"image_url": {"url": "https://example.com/product.png"}
}
]
}'Each reference is an HTTP(S) URL or a base64 data URL. Reference order is preserved; refer to images by position in the prompt when it matters. The current models accept up to 14 references.
Request fields
| Field | Required | Description |
|---|---|---|
model | Yes | Catalog model ID, for example gemini-3-pro-image. Do not use the upstream google/... ID |
prompt | Yes | Text instruction. For an edit, describe the change and what must stay the same |
resolution | No | Output tier. Current models accept 1K, 2K, or 4K |
aspect_ratio | No | Output ratio. Accepted values depend on the model |
n | No | Number of images. Current models accept 1 |
input_references | No | Reference images for editing or composition |
Current image models:
| Model ID | Resolutions | Aspect ratios | Images per task | References |
|---|---|---|---|---|
gemini-3-pro-image | 1K, 2K, 4K | 1:1, 2:3, 3:2, 3:4, 4:3, 4:5, 5:4, 9:16, 16:9, 21:9 | 1 | Up to 14 |
gemini-nano-banana-2.1 | 1K, 2K, 4K | 1:1, 1:4, 1:8, 2:3, 3:2, 3:4, 4:1, 4:3, 4:5, 5:4, 8:1, 9:16, 16:9, 21:9 | 1 | Up to 14 |
To pin a supplier, send the X-Felo-Channel header with a channel code from the model's channels list. See Channels and Provider Routing.
Find an image model
There is no separate image model list endpoint. Query the general catalog and filter by model_kind:
curl "https://openapi.felo.ai/api/v1/models" \
-H "Authorization: Bearer ${FELO_API_KEY}" \
| jq '.data[] | select(.model_kind == "image_generation") | {id, name}'GET /api/v1/models/{model_id} returns the model's channels and published rates.
supported_parameters in the general catalog is a protocol-level list. For image models it does not reliably enumerate the image fields above, so do not build your request from it. Use the field table in this guide and confirm the model ID from the catalog.
Pricing
Image generation is billed from the model's published rates. The catalog exposes input_image and output_image alongside the text prompt and completion rates. The image rates are token rates: resolution changes how many output image tokens an image consumes, so a 4K image costs more than a 1K image. Reference images add input image tokens.
The task response does not include a usage object. Check Console Billing for the charge.
A task that ends failed or interrupted produces no output and is not billed. A submit rejected with 402 did not create a task.
Errors
Endpoint-level errors from the image route use the OpenAI error envelope:
{
"error": {
"message": "Model kind does not match this generation endpoint",
"type": "invalid_request_error",
"param": null,
"code": "model_kind_mismatch"
}
}The request ID is in the X-Request-Id response header. Log it with the task ID.
| HTTP | error.code | Meaning |
|---|---|---|
| 400 | invalid_request_error | model is missing, or the body is not a JSON object |
| 400 | model_not_configured | The model ID is not in the enabled catalog |
| 400 | chat_model_not_allowed | A chat model was sent to the image endpoint |
| 400 | model_kind_mismatch | A video or other non-image model was sent |
| 402 | insufficient_balance | Not enough credits; no task was created |
| 404 | generation_task_not_found | The task ID is unknown or belongs to another account |
| 429 | daily_spend_limit_exceeded | The account's daily spend limit was reached |
| 503 | generation_unavailable | Generation is temporarily unavailable; retry later |
Authentication errors are described in Errors, Limits, and Retries.