Rendley docs

Text to speech

Text-to-speech turns a script into spoken audio in one call. Pick a voice, send the text, and get back an MP3. Place it on a timeline or use it on its own.

The flow

GET  /v1/ai/text-to-speech/voices           ← browse available voices
POST /v1/ai/text-to-speech                   ← send text, get a job
GET  /v1/jobs/{id}                           ← poll until completed

List available voices

Each voice comes back as id, name, model_id and preview_audio_url. Listen to the preview to pick one.

const API_KEY = "YOUR_API_KEY";
const API = "https://api.rendley.com/v1";

const headers = { "Authorization": `Bearer ${API_KEY}` };

const response = await fetch(`${API}/ai/text-to-speech/voices`, { headers });
const payload = await response.json();

const voices = payload.data;

for (const voice of voices) {
  console.log(voice.id.padEnd(24), voice.name.padEnd(20), voice.preview_audio_url || "");
}
import requests

API_KEY = "YOUR_API_KEY"
API = "https://api.rendley.com/v1"

HEADERS = {"Authorization": f"Bearer {API_KEY}"}

response = requests.get(f"{API}/ai/text-to-speech/voices", headers=HEADERS)
response.raise_for_status()

voices = response.json()["data"]

for voice in voices:
    print(f"{voice['id']:24} {voice['name']:20} {voice.get('preview_audio_url', '')}")

The list is paginated. Pass page and limit to walk it, or query to search by name.

Generate a voiceover

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
// Pick a voice id from GET /ai/text-to-speech/voices.
const VOICE_ID = "EXAVITQu4vr4xnSDxMaL";

const SCRIPT = [
  "Welcome to our product demo. In the next sixty seconds,",
  "you'll see how easy it is to automate video creation with",
  "the Rendley API.",
].join(" ");


// Queue the voiceover. This returns before any audio is rendered.
async function startSpeechJob() {
  const response = await fetch(`${API}/ai/text-to-speech`, {
    method: "POST",
    headers,
    body: JSON.stringify({
      params: {
        prompt: SCRIPT,
        voice_id: VOICE_ID,
      },
    }),
  });

  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 startSpeechJob();
const job = await waitForJob(jobId);

// The signed URL is the result. Fetch it, pipe it to your storage,
// or hand it to the browser.
console.log(job.output.url);
import requests

# Pick a voice id from GET /ai/text-to-speech/voices.
VOICE_ID = "EXAVITQu4vr4xnSDxMaL"

SCRIPT = (
    "Welcome to our product demo. In the next sixty seconds, "
    "you'll see how easy it is to automate video creation with "
    "the Rendley API."
)


def start_speech_job():
    """Queue the voiceover. Returns before any audio is rendered."""
    response = requests.post(
        f"{API}/ai/text-to-speech",
        headers=HEADERS,
        json={
            "params": {
                "prompt": SCRIPT,
                "voice_id": VOICE_ID,
            }
        },
    )
    response.raise_for_status()

    return response.json()["data"]["job_id"]


job_id = start_speech_job()
job = wait_for_job(job_id)

# The finished job points at a signed URL. Download it before it expires.
audio = requests.get(job["output"]["url"])
audio.raise_for_status()

with open("voiceover.mp3", "wb") as f:
    f.write(audio.content)

print("Saved voiceover.mp3")

Combining TTS with video

Generate the voiceover, then use the automated editing agent to lay it over footage.

{
  "prompt": "Add voiceover.mp3 as narration over the product-demo.mp4 footage. Lower the original audio to 20% volume.",
  "files": [
    { "url": "https://cdn.example.com/product-demo.mp4", "name": "product-demo.mp4" },
    { "url": "JOB_OUTPUT_URL", "name": "voiceover.mp3" }
  ]
}

Pass the voiceover job’s output.url as the second file. Set name on each entry so the prompt can refer to the file by that name. Without it the agent derives the name from the URL, and a signed URL gives an unusable one.