Rendley docs

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

Modelmodel_idGood forEdits
Nano Banananano-bananaThe default. Image-to-image editing.Yes
Nano Banana Pronano-banana-proHigher fidelity than the default.Yes
FLUX 1.1 Proflux-1.1-proPhotoreal detail and prompt adherence.No
Imagen 4imagen-4Text rendering and clean composition.No
Seedream 4.5seedream-4.5Stylized and illustrative work.Yes

Generate an image has a dropdown listing all 15 models with the exact parameters each one accepts.