Generate an image
Image generation uses the standard job pattern. Start a job, poll it, then download what the finished job points at.
POST https://api.rendley.com/v1/ai/generate-image
The script
Omitting model_id uses Nano Banana, the default.
The polling helpers used below
const API_KEY = "YOUR_API_KEY";
const API = "https://api.rendley.com/v1";
const headers = {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json",
};
// Both endpoints share these three terminal statuses. They differ only in
// their in-progress names: queued/processing vs pending/running.
const TERMINAL = ["completed", "failed", "canceled"];
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// Generation and export jobs. The finished job carries output.url.
async function waitForJob(jobId, interval = 5000) {
let job = null;
while (job === null || !TERMINAL.includes(job.status)) {
await sleep(interval);
const response = await fetch(`${API}/jobs/${jobId}`, { headers });
if (!response.ok) {
throw new Error("Job lookup failed: " + response.status);
}
const body = await response.json();
job = body.data;
console.log("Job status:", job.status);
}
if (job.status !== "completed") {
throw new Error("Job " + job.status + ": " + (job.error || ""));
}
return job;
}
// Agent jobs. The finished job carries project_id, thread_id and
// last_message, but no output: export the project to get a file.
// onPause runs when an interactive run stops to ask something.
async function waitForAgentJob(jobId, interval = 5000, onPause) {
let job = null;
while (job === null || !TERMINAL.includes(job.status)) {
// This endpoint long-polls. Sleep anyway, so a fast response
// cannot turn this into a tight request loop.
await sleep(interval);
const response = await fetch(`${API}/agent/jobs/${jobId}`, { headers });
if (!response.ok) {
throw new Error("Job lookup failed: " + response.status);
}
const body = await response.json();
job = body.data;
console.log("Edit status:", job.status);
// Only interactive runs reach this; unattended runs never pause.
if (job.status === "waiting_input" && onPause) {
await onPause(job);
}
}
if (job.status !== "completed") {
throw new Error("The edit did not finish: " + (job.error || job.reason));
}
return job;
}import time
import requests
API_KEY = "YOUR_API_KEY"
API = "https://api.rendley.com/v1"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
# Both endpoints share these three terminal statuses. They differ only in
# their in-progress names: queued/processing vs pending/running.
TERMINAL = {"completed", "failed", "canceled"}
def wait_for_job(job_id, interval=5):
"""Generation and export jobs. The finished job carries output.url."""
job = None
while job is None or job["status"] not in TERMINAL:
time.sleep(interval)
response = requests.get(f"{API}/jobs/{job_id}", headers=HEADERS)
response.raise_for_status()
job = response.json()["data"]
print("Job status:", job["status"])
if job["status"] != "completed":
raise RuntimeError(f"Job {job['status']}: {job.get('error', '')}")
return job
def wait_for_agent_job(job_id, interval=5, on_pause=None):
"""Agent jobs. The finished job carries project_id, thread_id and
last_message, but no output: export the project to get a file.
on_pause runs when an interactive run stops to ask something.
"""
job = None
while job is None or job["status"] not in TERMINAL:
# This endpoint long-polls. Sleep anyway, so a fast response
# cannot turn this into a tight request loop.
time.sleep(interval)
response = requests.get(f"{API}/agent/jobs/{job_id}", headers=HEADERS)
response.raise_for_status()
job = response.json()["data"]
print("Edit status:", job["status"])
# Only interactive runs reach this; unattended runs never pause.
if job["status"] == "waiting_input" and on_pause:
on_pause(job)
if job["status"] != "completed":
raise RuntimeError("The edit did not finish: " + (job.get("error") or job.get("reason", "")))
return job// Queue the generation. This returns before the image exists.
async function startImageJob() {
const response = await fetch(`${API}/ai/generate-image`, {
method: "POST",
headers,
body: JSON.stringify({
params: {
prompt: "A product shot of a ceramic mug on a linen cloth, soft daylight",
aspect_ratio: "1:1",
},
}),
});
if (!response.ok) {
throw new Error("Could not start the job: " + response.status);
}
const payload = await response.json();
return payload.data.job_id;
}
const jobId = await startImageJob();
const job = await waitForJob(jobId, 2000);
// The signed URL is the result. Fetch it, pipe it to your storage,
// or hand it to the browser.
console.log(job.output.url);
console.log(job.output.mime_type + ", " + job.output.size + " bytes");def start_image_job():
"""Queue the generation. Returns before the image exists."""
response = requests.post(
f"{API}/ai/generate-image",
headers=HEADERS,
json={
"params": {
"prompt": "A product shot of a ceramic mug on a linen cloth, soft daylight",
"aspect_ratio": "1:1",
},
},
)
response.raise_for_status()
return response.json()["data"]["job_id"]
job_id = start_image_job()
job = wait_for_job(job_id, 2)
# The finished job points at a signed URL. Download it before it expires.
image = requests.get(job["output"]["url"])
image.raise_for_status()
with open("image.png", "wb") as out:
out.write(image.content)
print(f"Saved image.png ({job['output']['size']} bytes)")Editing an existing image
Pass image_inputs to transform an existing image rather than generate from scratch. Change a pose, restyle, add or remove elements.
{
"params": {
"prompt": "Put the mug on a dark slate surface instead",
"image_inputs": ["https://cdn.example.com/mug.png"],
"aspect_ratio": "match_input_image"
}
}
match_input_image keeps the source image’s aspect ratio. Use it when editing rather than generating.
Each entry in image_inputs takes a public URL or a library file hash.
Not every model edits. image_inputs exists on Nano Banana, Nano Banana Pro, FLUX 2 Max, GPT Image 2, and the Seedream models. A model that does not declare the parameter ignores it, so the job succeeds and returns a fresh generation rather than an edit. Check the model’s schema before you send it. match_input_image is narrower still: GPT Image 2 takes image_inputs but not that aspect ratio.
Choosing a model
| Model | model_id | Good for | Edits |
|---|---|---|---|
| Nano Banana | nano-banana | The default. Image-to-image editing. | Yes |
| Nano Banana Pro | nano-banana-pro | Higher fidelity than the default. | Yes |
| FLUX 1.1 Pro | flux-1.1-pro | Photoreal detail and prompt adherence. | No |
| Imagen 4 | imagen-4 | Text rendering and clean composition. | No |
| Seedream 4.5 | seedream-4.5 | Stylized and illustrative work. | Yes |
Generate an image has a dropdown listing all 15 models with the exact parameters each one accepts.