New: the storyboard. Redraw or revoice one scene without redoing the video.

See how it works
Doodaro
Quickstart · about 5 minutes of your time

Your first Doodaro video from the API

Eight requests take you from a new key to a downloaded MP4. You write two scenes, Doodaro draws and narrates them, and the finished project also opens in your dashboard.

Before you start

  • A Doodaro account. Create one free, no card needed. Every account has its own workspace and its own API keys.
  • A terminal with curl. For the script at the end, Node.js 18 or newer.
  • The 5 minutes are your time. Generation and rendering run on the server and take as long as they take; you poll while they work.

1. Create a key

Open Developer in your dashboard (bottom of the sidebar, above Settings). Name the key, for example Quickstart, and create it. Copy the secret now: it is shown once.

terminal
export DOODARO_API_KEY="ddr_live_..."

For an app, put it in .env instead and keep that file out of git.

2. Check the key

get_account
curl https://app.doodaro.com/api/v1/me \
  -H "Authorization: Bearer $DOODARO_API_KEY"

You get your plan, the minute counters and the key's name and prefix. 401 unauthorized means the key is missing, mistyped or revoked.

3. Pick a style and a voice

list_styles
curl "https://app.doodaro.com/api/v1/styles?query=chalkboard" \
  -H "Authorization: Bearer $DOODARO_API_KEY"

Copy one style id. It looks like doodaro:<versionId>.

list_voices
curl "https://app.doodaro.com/api/v1/voices?language=en" \
  -H "Authorization: Bearer $DOODARO_API_KEY"

Copy one voice id and its name. Always use real ids from these lists. Never make one up.

4. Create the project

Send your scenes. Each one has narration (what the voice says) and a visual direction (what gets drawn). Replace the two placeholders with the ids from step 3.

create_project
curl -X POST https://app.doodaro.com/api/v1/projects \
  -H "Authorization: Bearer $DOODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "How planes fly",
    "format": "portrait",
    "styleId": "doodaro:<versionId>",
    "voice": {"id": "<voice id>", "name": "<voice name>"},
    "language": "en",
    "scenes": [
      {"title": "Hook",
       "narration": "Every day, around a hundred thousand flights take off. Here is how a wing holds them up.",
       "visualDirection": "A busy airport at dawn, planes lined up on the runway."},
      {"title": "Lift",
       "narration": "A wing pushes air down. The air pushes the wing up. That push is called lift.",
       "visualDirection": "A wing cross-section with arrows showing air flowing down and lift pointing up."}
    ]
  }'

The response is 201 with the full project detail. Save the id, and open dashboardUrl if you want to watch the storyboard fill in.

terminal
export PROJECT_ID="<id from the response>"

5. Generate pictures and narration

generate_assets
curl -X POST https://app.doodaro.com/api/v1/projects/$PROJECT_ID/generate \
  -H "Authorization: Bearer $DOODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: quickstart-generate-1" \
  -d '{}'

An empty body means: every scene, pictures and voices, skipping anything already ready. The response is 202 with one job per kind:

Response 202
{
  "jobs": [
    {"id": "uuid", "kind": "image", "type": "batch", "sceneCount": 2},
    {"id": "uuid", "kind": "voice", "type": "batch", "sceneCount": 2}
  ]
}

The Idempotency-Key header means you can safely resend this exact request if your connection drops. You get the same jobs back instead of a second set.

6. Poll the jobs, then check the project

get_job
# Repeat every 3 to 5 seconds for each job id until status is terminal
curl https://app.doodaro.com/api/v1/jobs/$JOB_ID \
  -H "Authorization: Bearer $DOODARO_API_KEY"

Wait until each job's status is complete. failed, cancelled and uncertain also end the job; read error and the project before retrying. Then read the project. The server applies the finished pictures and narration first.

get_project
curl https://app.doodaro.com/api/v1/projects/$PROJECT_ID \
  -H "Authorization: Bearer $DOODARO_API_KEY"
# look for "readiness": {"canRender": true, "blockers": []}

If canRender is false, blockers says why, for example a scene without a picture description.

7. Render the video

render_video
curl -X POST https://app.doodaro.com/api/v1/projects/$PROJECT_ID/renders \
  -H "Authorization: Bearer $DOODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: quickstart-render-1" \
  -d '{"kind": "video"}'

The response is 202 with a render object. Poll its id with GET /jobs/{id} the same way as step 6.

8. Download the MP4

When the render job is complete, its result has videoUrl and finalFrameUrl. The video link is public, so no key is needed.

terminal
# after get_job on the render id returns "status": "complete"
curl -L "<result.videoUrl>" -o how-planes-fly.mp4

That's it. The same video is in your dashboard under the project, where you can redraw or revoice one scene and render again.

The same flow in JavaScript

One file, no dependencies. Save it as doodaro-flow.mjs and run DOODARO_API_KEY=ddr_live_... node doodaro-flow.mjs. The Python version is in the API reference.

doodaro-flow.mjs
// doodaro-flow.mjs
// Node.js 18 or newer (built-in fetch). Run on a server, not in a browser.
//   DOODARO_API_KEY=ddr_live_... node doodaro-flow.mjs
import { writeFile } from 'node:fs/promises';
import { randomUUID } from 'node:crypto';

const BASE = 'https://app.doodaro.com/api/v1';
const KEY = process.env.DOODARO_API_KEY;
if (!KEY) throw new Error('Set DOODARO_API_KEY first.');

const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
const TERMINAL = new Set(['complete', 'failed', 'cancelled', 'uncertain']);

// One helper for every call: auth header, JSON body, error object, 429 retry.
async function api(method, path, body, { idempotencyKey } = {}) {
  for (let attempt = 0; ; attempt++) {
    const headers = { Authorization: `Bearer ${KEY}` };
    if (body !== undefined) headers['Content-Type'] = 'application/json';
    if (idempotencyKey) headers['Idempotency-Key'] = idempotencyKey;

    const res = await fetch(BASE + path, {
      method,
      headers,
      body: body === undefined ? undefined : JSON.stringify(body),
    });

    if (res.status === 429 && attempt < 5) {
      const seconds = Number(res.headers.get('Retry-After')) || 5;
      await sleep(seconds * 1000);
      continue;
    }

    const data = await res.json().catch(() => ({}));
    if (!res.ok) {
      const error = new Error(`${res.status} ${data.error?.code}: ${data.error?.message}`);
      error.status = res.status;
      error.code = data.error?.code;
      throw error;
    }
    return data;
  }
}

async function waitForJob(jobId) {
  for (;;) {
    const job = await api('GET', `/jobs/${jobId}`);
    console.log(`${job.type} job ${job.id}: ${job.status} ${job.progress ?? 0}%`);
    if (TERMINAL.has(job.status)) return job;
    await sleep(4000); // poll every 3 to 5 seconds
  }
}

// 1. Pick a style and a voice. Never invent ids.
const { styles } = await api('GET', '/styles?query=chalkboard');
const { voices } = await api('GET', '/voices?language=en');
const style = styles[0];
const voice = voices[0];

// 2. Create a project with your own scenes.
let project = await api('POST', '/projects', {
  title: 'How planes fly',
  format: 'portrait',
  styleId: style.id,
  voice: { id: voice.id, name: voice.name },
  language: 'en',
  scenes: [
    {
      title: 'Hook',
      narration: 'Every day, around a hundred thousand flights take off. Here is how a wing holds them up.',
      visualDirection: 'A busy airport at dawn, planes lined up on the runway.',
    },
    {
      title: 'Lift',
      narration: 'A wing pushes air down. The air pushes the wing up. That push is called lift.',
      visualDirection: 'A wing cross-section with arrows showing air flowing down and lift pointing up.',
    },
  ],
});
console.log('Project:', project.id, project.dashboardUrl);

// 3. Generate pictures and narration. The Idempotency-Key makes a retry safe.
const run = randomUUID();
const { jobs } = await api('POST', `/projects/${project.id}/generate`, {}, { idempotencyKey: `generate-${run}` });
const finished = await Promise.all(jobs.map(job => waitForJob(job.id)));
if (finished.some(job => job.status !== 'complete')) throw new Error('Generation did not complete.');

// 4. Read the project. get_project syncs finished assets first.
project = await api('GET', `/projects/${project.id}`);
if (!project.readiness.canRender) {
  throw new Error(`Not ready to render: ${JSON.stringify(project.readiness.blockers)}`);
}

// 5. Render the video and wait for it.
const { render } = await api('POST', `/projects/${project.id}/renders`, {}, { idempotencyKey: `render-${run}` });
const done = await waitForJob(render.id);
if (done.status !== 'complete') throw new Error(`Render ${done.status}: ${JSON.stringify(done.error)}`);

// 6. Download the MP4. videoUrl is a public link, so no key is sent.
const video = await fetch(done.result.videoUrl);
await writeFile('how-planes-fly.mp4', Buffer.from(await video.arrayBuffer()));
console.log('Saved how-planes-fly.mp4. Open in the dashboard:', project.dashboardUrl);

Troubleshooting

You seeWhat to do
401 unauthorizedCheck the header is Authorization: Bearer ddr_live_... and the key is not revoked in Developer.
400 invalid_requestRead message. Common causes: no scenes or script, or a narration over 4,000 characters.
415 unsupported_media_typeAdd -H "Content-Type: application/json" and send valid JSON.
409 not_ready on renderA picture or narration is missing or stale. Read readiness.blockers, generate again, then render.
429 rate_limitedWait the seconds in Retry-After. Production limits are 20 active jobs, 60 per hour and 200 per day.
503 unavailableA provider or storage is down. Retry later with the same Idempotency-Key.
Captions missing from the MP4Expected in the beta. Caption settings are saved but not drawn into exports yet.

Next steps

  • API reference: edit scenes, use a script instead of scenes, plan with the story assistant, render one scene as a preview.
  • Copy for AI and MCP: let Claude Code, Codex or Cursor run this flow for you.

Rather make it in the studio?

The API drives the same studio. Start with 2 free minutes, or open Developer in your dashboard to create a key.