> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lavendly.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Node SDK

> Generate videos from Node. Cookbook patterns for typical agent workflows.

## Install

```bash theme={null}
npm i @lavendly/sdk
```

```js theme={null}
import { Lavendly } from '@lavendly/sdk';

const vs = new Lavendly({
  apiKey: process.env.LAVENDLY_API_KEY,
  // optional, defaults to https://api.lavendly.ai
  baseURL: process.env.LAVENDLY_API,
});
```

## A render in 10 lines

```js theme={null}
const wf = await vs.workflows.create({
  name: "Fox in a bookshop",
  shots: [{ prompt: "a sleepy fox in a bookshop discovering an old map",
            duration: 5 }],
});

// .run() creates a render and polls until done. Returns the result.
const { video_url } = await vs.renders.run(wf.id);
console.log(video_url);
```

## Narrated short with music

```js theme={null}
const wf = await vs.workflows.create({
  name: "Barista at dawn",
  shots: [{ prompt: "a barista pulling an espresso shot at dawn",
            duration: 6 }],
});

await vs.audio.attachTrack(wf.id, "shot_1", {
  kind: "voiceover",
  script: "Forty seconds. The whole café holds its breath.",
  subtitleStyle: "tiktok",
  idempotencyKey: `vo-${wf.id}`,
});

await vs.audio.attachTrack(wf.id, "shot_1", {
  kind: "music",
  mood: "warm low jazz piano",
  ducking: true,
  volume: 0.4,
  idempotencyKey: `music-${wf.id}`,
});

await vs.audio.setClipNativeAudio(wf.id, "shot_1", {
  mode: "mix",
  volume: 0.6,
});

const { video_url } = await vs.renders.run(wf.id, {
  idempotencyKey: `render-${wf.id}`,
});
```

## Multi-clip sequence with shared anchors

```js theme={null}
const character = "A weathered explorer, mid-40s, dusty leather jacket, sunburnt skin.";
const palette   = "Amber and deep teal, golden-hour cinematic lighting.";

const wf = await vs.workflows.create({
  name: "Cave sequence",
  shots: [
    { prompt: `${character}\n${palette}\nEnters a cave mouth, torch in hand.`,
      duration: 3 },
    { prompt: `${character}\n${palette}\nFinds a glowing crystal embedded in rock.`,
      duration: 3 },
    { prompt: `${character}\n${palette}\nSteps back in awe, eyes wide.`,
      duration: 3 },
  ],
});

// One voiceover bridging the whole sequence
await vs.audio.attachTrack(wf.id, "shot_1", {
  kind: "voiceover",
  script: "He had been searching for years. He did not expect to find it here.",
  subtitleStyle: "cinematic",
  idempotencyKey: `vo-${wf.id}`,
});

const { video_url } = await vs.renders.run(wf.id);
```

## Polling manually

`renders.run` is a convenience that polls for you. To control polling
yourself:

```js theme={null}
const job = await vs.renders.create(wf.id, {
  idempotencyKey: `render-${wf.id}`,
});

for (;;) {
  const status = await vs.renders.get(wf.id, job.job_id);
  console.log(`${status.status}, ${Math.round(status.progress * 100)}%`);
  if (status.status === "done")   return status.result.video_url;
  if (status.status === "failed") throw new Error(status.error);
  await new Promise((r) => setTimeout(r, 4000));
}
```

## Budget discipline

Before a paid action, check the ledger:

```js theme={null}
const { available, plan_pool } = await vs.ledger.get();
if (available < 20) throw new Error(`Budget too low: ${available} credits`);
```

And cap monthly spend:

```js theme={null}
const { fraction_used } = await vs.usage.monthly();
if (fraction_used > 0.85) {
  console.warn("Monthly budget over 85% used, pausing");
  return;
}
```

## Error handling

Every SDK error carries the same envelope as the API:

```js theme={null}
try {
  await vs.workflows.create({ name: "huge", shots: bigArray });
} catch (err) {
  console.error(err.code);    // 'workflow_too_large'
  console.error(err.message); // human-readable
  console.error(err.status);  // 413
}
```

Standard codes: `unauthenticated`, `invalid_args`,
`workflow_not_found`, `clip_not_found`, `track_not_found`,
`job_not_found`, `insufficient_credits`, `monthly_cost_cap`,
`workflow_too_large`. See [Errors](/api-reference/errors).

## Idempotency in one line

Every paid mutation on the SDK accepts `idempotencyKey`. Generate it
deterministically per logical action, same retry, same response:

```js theme={null}
const key = `render-${wf.id}-${dateBucket()}`;
await vs.renders.create(wf.id, { idempotencyKey: key });
```

Two calls with the same `key` within 5 minutes return the same job ID.
The user is billed once.
