Felo API PlatformFelo API Platform

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

How it differs from the OpenAI Images API

The path is similar to OpenAI's Images API. The response is not:

OpenAI Images APIFelo Image API
Submit response200 with the image data202 with task_id, status: "queued"
When the image existsIn the submit responseAfter polling reaches succeeded
Image payloadb64_json or urlA time-limited signed url
Request errorsHTTP error on submitUsually 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}"
StatusMeaningWhat to do
queuedAccepted; waiting to startKeep polling
runningThe model is generatingKeep polling
succeededTerminal; outputs is populatedDownload the outputs
failedTerminal; read error.messageFix the request; do not retry unchanged
interruptedTerminal; the result could not be recoveredResubmit 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=..."
    }
  ]
}
  • outputs is empty until the task succeeds.
  • url is a signed URL. It downloads without an API key, so treat it as a bearer link.
  • The URL is time-limited. The exp query 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

FieldRequiredDescription
modelYesCatalog model ID, for example gemini-3-pro-image. Do not use the upstream google/... ID
promptYesText instruction. For an edit, describe the change and what must stay the same
resolutionNoOutput tier. Current models accept 1K, 2K, or 4K
aspect_ratioNoOutput ratio. Accepted values depend on the model
nNoNumber of images. Current models accept 1
input_referencesNoReference images for editing or composition

Current image models:

Model IDResolutionsAspect ratiosImages per taskReferences
gemini-3-pro-image1K, 2K, 4K1:1, 2:3, 3:2, 3:4, 4:3, 4:5, 5:4, 9:16, 16:9, 21:91Up to 14
gemini-nano-banana-2.11K, 2K, 4K1: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:91Up 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.

HTTPerror.codeMeaning
400invalid_request_errormodel is missing, or the body is not a JSON object
400model_not_configuredThe model ID is not in the enabled catalog
400chat_model_not_allowedA chat model was sent to the image endpoint
400model_kind_mismatchA video or other non-image model was sent
402insufficient_balanceNot enough credits; no task was created
404generation_task_not_foundThe task ID is unknown or belongs to another account
429daily_spend_limit_exceededThe account's daily spend limit was reached
503generation_unavailableGeneration is temporarily unavailable; retry later

Authentication errors are described in Errors, Limits, and Retries.