# TypeScript SDK

> Install @nodaro/sdk, authenticate with an API token, and run Nodaro nodes and workflows from TypeScript in Node.js, the browser and edge runtimes.

Source: https://nodaro.ai/docs/developers/sdk

The **Nodaro TypeScript SDK** is the typed client for the Nodaro REST API, published on npm as `@nodaro/sdk`. It runs single nodes and whole workflows, waits for their results, and manages characters, voices, media and more. It works in Node.js, in the browser and on edge runtimes, and every API error arrives as a typed error class you can catch.

## Install

```bash
npm install @nodaro/sdk
```

- The package is open source under the Apache-2.0 license. See [@nodaro/sdk on npm](https://www.npmjs.com/package/@nodaro/sdk) and [its source on GitHub](https://github.com/nodaroai/app.nodaro.ai/tree/main/packages/client).
- It ships ES module and CommonJS builds with TypeScript type definitions.
- It needs only a global `fetch`, so it runs on Node.js 20 or newer, in modern browsers, in React Native, on Cloudflare Workers, on Deno and on Bun.
- The [Nodaro CLI](https://nodaro.ai/docs/developers/cli) is built on this SDK, so a CLI command and an SDK call reach the same endpoints.

## Get an API token

### Create the token

Sign in to [Nodaro](https://app.nodaro.ai), open **Settings › API Tokens** and click **Create Token**.

### Copy it once

The token starts with `ndr_` and is shown only once. Nodaro stores only a hash of it, so a lost token cannot be shown again. Create a new one instead.

### Keep it out of your code

Store the token in an environment variable, such as `NODARO_TOKEN`, or in your secret manager. Never put it in client-side code.

A personal token acts as you. To act for other people, for example in an app that many users connect to, use [OAuth](https://nodaro.ai/docs/developers/oauth) instead. [Authentication](https://nodaro.ai/docs/developers/sdk/auth) compares every option.

## Make your first call

```ts

const client = createClient({
baseUrl: "https://app.nodaro.ai",
auth: new StaticTokenAuth(process.env.NODARO_TOKEN!),
})

const { data: nodes } = await client.nodes.list()
console.log(`${nodes.length} node types available`)
```

`client.nodes.list()` costs nothing and needs no scopes, so it is a good first test of the connection. On a self-hosted install, set `baseUrl` to the address of your instance. In a browser app that is served from the same origin as Nodaro, use an empty string. [The client](https://nodaro.ai/docs/developers/sdk/client) lists every option of `createClient`.

## Generate an image, then a video

```ts
const image = await client.nodes.runAndWait("generate-image", {
prompt: "A snow leopard resting on a rock at sunrise",
provider: "nano-banana-2",
})
console.log(image.imageUrl)

const video = await client.nodes.runAndWait("generate-video", {
prompt: "The snow leopard slowly turns its head toward the camera",
imageUrl: image.imageUrl,
provider: "seedance-2-fast",
duration: 4,
})
console.log(video.videoUrl)
```

`runAndWait` starts the run, polls it, and resolves with the job's output: `imageUrl` for images, `videoUrl` for video and `audioUrl` for audio. The first call runs the [Generate Image](https://nodaro.ai/docs/nodes/image/generate-image) node on [Nano Banana 2](https://nodaro.ai/docs/models/image/nano-banana-2). The second call animates that image with the [Generate Video](https://nodaro.ai/docs/nodes/video/generate-video) node on [Seedance 2 Fast](https://nodaro.ai/docs/models/video/seedance-2-fast).

Leave out `provider` to use the node's default model. Each node page lists the models it can run and their credit prices, and `client.models.list()` returns the same catalog from code. See [Models and credits](https://nodaro.ai/docs/developers/sdk/models-and-credits).

## How runs work

Generation is asynchronous. A request starts a **job** on a worker and returns at once. The result arrives seconds or minutes later.

| You call | You get back | What to do next |
| --- | --- | --- |
| `client.nodes.run(type, params)` | `{ jobId }` | Poll `client.jobs.getStatus(jobId)` until the status is `completed`, `failed` or `cancelled`. |
| `client.nodes.runAndWait(type, params, opts)` | The job's output | Nothing. The SDK polls every 2 seconds, for up to 15 minutes. |
| `client.nodes.runMany(type, paramsList, opts)` | One `{ jobId, output }` per request | Nothing. The runs start together and resolve in input order. |
| `client.workflows.run(id)` | `{ executionId, status }` | Poll `client.executions.get(executionId)` until the run ends. |

A few node types, such as [Combine Text](https://nodaro.ai/docs/nodes/automate/combine-text), run inline and return their result directly, without a `jobId`. [Run nodes](https://nodaro.ai/docs/developers/sdk/nodes) explains every run method, and [Jobs and executions](https://nodaro.ai/docs/developers/sdk/jobs-and-executions) explains the statuses.

### Show progress and let the user stop waiting

```ts

const controller = new AbortController()
stopButton.onclick = () => controller.abort()

try {
const clip = await client.nodes.runAndWait(
"generate-video",
{ prompt: "Waves roll onto a black sand beach", provider: "seedance-2-fast", duration: 4 },
{
signal: controller.signal,
onProgress: (status) => setProgressBar(status.progress ?? 0),
},
)
showVideo(clip.videoUrl)
} catch (err) {
if (err instanceof JobAbortedError && err.jobId) {
await client.jobs.cancel(err.jobId)
} else {
throw err
}
}
```

`onProgress` receives the job status on every poll, with `progress` from 0 to 100 when the model reports it. Aborting the signal only stops the waiting. The job keeps running until you cancel it with `client.jobs.cancel(jobId)`, which also refunds the credits it had reserved.

Show each result as soon as it arrives. In a two-step flow, display the image while the video step is still running.

## Choose how to authenticate

| Provider | Use it when | Token source |
| --- | --- | --- |
| `StaticTokenAuth` | Server code with one fixed token | An API token (`ndr_...`) or an OAuth access token (`ndr_app_...`) |
| `CallbackAuth` | You refresh or rotate tokens yourself | Your function, called before every request |
| `supabaseAuth` | A browser app whose users sign in to the same Nodaro instance | The user's live session |

Every request asks the provider for a token and sends it as `Authorization: Bearer <token>`. When the provider returns `null`, the request goes out without the header. See [Authentication](https://nodaro.ai/docs/developers/sdk/auth) for each provider and for browser-specific rules.

## Handle errors

Every method throws a typed subclass of `NodaroError` when the API answers with an error. Catch the specific classes first and `NodaroError` last.

```ts

InsufficientCreditsError,
NodaroError,
RateLimitedError,
UnauthorizedError,
} from "@nodaro/sdk"

try {
await client.workflows.run(workflowId)
} catch (err) {
if (err instanceof InsufficientCreditsError) {
showPaywall({ required: err.required, available: err.available })
} else if (err instanceof UnauthorizedError) {
askForANewToken()
} else if (err instanceof RateLimitedError) {
retryLater()
} else if (err instanceof NodaroError) {
console.error(`API error ${err.status} (${err.code}): ${err.message}`)
} else {
throw err // a network failure, not an API answer
}
}
```

Every `NodaroError` has a `message`, a stable `code` such as `insufficient_credits`, and the HTTP `status`. [Errors](https://nodaro.ai/docs/developers/sdk/errors) lists every class and when it is thrown.

## Common recipes

### Run a workflow and wait for it

`client.workflows.run()` starts an **execution**, one run of every node in the workflow, and returns at once. Poll `client.executions.get()` until the status is terminal.

```ts
const { executionId } = await client.workflows.run(workflowId)

for (;;) {
const { data } = await client.executions.get(executionId)
console.log(`${data.completedNodes}/${data.totalNodes} nodes done`)

if (["completed", "failed", "cancelled", "timed_out"].includes(data.status)) {
if (data.status !== "completed") throw new Error(data.errorMessage ?? data.status)
console.log(`Done. Used ${data.totalCreditsUsed} credits.`)
break
}
await new Promise((resolve) => setTimeout(resolve, 2_000))
}
```

Pass `{ nodeIds: [...] }` as the second argument to run only some nodes. See [Workflows and projects](https://nodaro.ai/docs/developers/sdk/workflows).

### Generate several candidates at once

```ts
const results = await client.nodes.runMany("generate-image", [
{ prompt: "A lighthouse at dawn, watercolor" },
{ prompt: "A lighthouse at dusk, watercolor" },
{ prompt: "A lighthouse in a storm, watercolor" },
])
for (const { jobId, output } of results) console.log(jobId, output.imageUrl)
```

`runMany` rejects when any run fails. To let a model choose the best result, pass the URLs to [`client.reduce.run()`](https://nodaro.ai/docs/developers/sdk/llm-and-reduce).

### Upload a file and use it

```ts
const upload = await client.uploads.upload(file) // a File, in the browser or Node.js
const portrait = await client.nodes.runAndWait("generate-image", {
prompt: "The same person as a watercolor portrait",
referenceImageUrls: [upload.url],
})
```

The upload returns a public `url` you can pass to any node that takes an image, a video or an audio URL. See [Media and uploads](https://nodaro.ai/docs/developers/sdk/media-and-uploads).

### Check the price before you run

```ts
const { total } = await client.credits.balance()
const { data: prices } = await client.credits.modelCosts(["nano-banana-pro", "nano-banana-pro:4K"])

if (total < prices["nano-banana-pro:4K"]) showPaywall()
```

Prices and balances exist on Nodaro Cloud. See [Models and credits](https://nodaro.ai/docs/developers/sdk/models-and-credits) and [Credits](https://nodaro.ai/docs/concepts/credits).

### Improve a prompt before you generate

```ts
const { prompt } = await client.promptHelper.enhance({
nodeType: "generate-image",
prompt: "snow leopard on a rock",
})
```

The [Prompt Wizard](https://nodaro.ai/docs/developers/sdk/pickers-and-prompts) rewrites a rough idea into a detailed prompt for the node you name. Each call costs credits.

## Use the SDK with an AI coding assistant

- **Claude Code plugin.** Run `/plugin marketplace add nodaroai/app.nodaro.ai`, then `/plugin install nodaro`. The plugin adds a skill that knows the SDK's patterns, models and credits, and connects Nodaro's hosted MCP server. See [Agent skills](https://nodaro.ai/docs/developers/agent-skills).
- **Other assistants.** The package README on npm starts with a short primer written for coding assistants. Paste it into Cursor or any other assistant together with your request.
- **No code at all.** To let an assistant run Nodaro for you, connect it over [MCP](https://nodaro.ai/docs/mcp).

## Reference

<Card title="Client" href="/docs/developers/sdk/client" description="createClient, every option, workspaces, and the full list of resources." />
<Card title="Authentication" href="/docs/developers/sdk/auth" description="StaticTokenAuth, CallbackAuth, supabaseAuth and shared browser sessions." />
<Card title="Errors" href="/docs/developers/sdk/errors" description="Every error class, its status and code, and how to recover." />
<Card title="Run nodes" href="/docs/developers/sdk/nodes" description="run, runAndWait and runMany, with typed parameters." />
<Card title="Workflows and projects" href="/docs/developers/sdk/workflows" description="Create, update, share, export and run workflows." />
<Card title="Jobs and executions" href="/docs/developers/sdk/jobs-and-executions" description="Poll, list, cancel and delete runs." />

The SDK wraps the same endpoints as the [REST API](https://nodaro.ai/docs/developers/api). For an endpoint that has no SDK method yet, call it with `client.request()`, which keeps the same authentication and typed errors.

## Frequently asked questions

### What is @nodaro/sdk?

It is the official TypeScript client for the Nodaro REST API. It runs nodes and workflows, waits for their results, and manages characters, voices, media and more, with a typed method and a typed error for every call.

### How do I authenticate the Nodaro SDK?

Create a token in Nodaro under Settings › API Tokens, store it in an environment variable, and pass it to createClient as new StaticTokenAuth(token). Browser apps and OAuth apps use supabaseAuth or CallbackAuth instead.

### Which runtimes does the SDK support?

Any runtime with a global fetch. That includes Node.js 20 or newer, modern browsers, React Native, Cloudflare Workers, Deno and Bun.

### Do I have to poll for results myself?

No. client.nodes.runAndWait starts a run, polls its job every 2 seconds, and resolves with the output URLs. Write your own loop with client.jobs.getStatus only when you need full control.

### Can I use the SDK with a self-hosted Nodaro install?

Yes. Set baseUrl to the address of your instance. Some resources, such as Studio productions, Recast and organizations, exist only on Nodaro Cloud and answer 404 elsewhere.
