# Client

> Create a Nodaro SDK client with createClient, set its base URL, auth, timeout and workspace, and find every resource the client exposes.

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

The **client** is the object `createClient()` returns: a `NodaroClient` that holds your base URL, your auth provider and your settings, and exposes every part of the Nodaro API as a resource, such as `client.workflows` or `client.nodes`. You create it once and reuse it for every call.

## Create a client

```ts

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

```ts
createClient(options: ClientOptions): NodaroClient
```

<TypeTable
type={{
baseUrl: {
type: 'string',
required: true,
description: "The Nodaro server, for example https://app.nodaro.ai or your self-hosted address. Use an empty string for same-origin requests in a browser app. A trailing slash is removed.",
},
auth: {
type: 'Auth',
required: true,
description: "The auth provider: new StaticTokenAuth(token), supabaseAuth(supabase), new CallbackAuth(fn), or any object with a getToken() method.",
typeDescriptionLink: '/docs/developers/sdk/auth',
},
fetch: {
type: 'typeof fetch',
default: 'globalThis.fetch',
description: "A custom fetch function, for tests, retries or tracing.",
},
timeoutMs: {
type: 'number',
default: '60000',
description: "The timeout of each request, in milliseconds. The request is aborted when it runs out.",
},
workspaceId: {
type: 'string',
description: "The workspace every request acts in, sent as the X-Nodaro-Workspace header. Omit it to work in your personal space. Nodaro Cloud organizations only.",
},
clientLabel: {
type: 'string',
default: "'sdk/<version>'",
description: "The value of the X-Nodaro-Client header. Nodaro records it as the origin of each job. Set it only when you build another tool on top of the SDK.",
},
}}
/>

`NodaroClient` is also exported as a class, so you can type a function that receives a client:

```ts

async function countWorkflows(client: NodaroClient, projectId: string) {
const { data } = await client.workflows.list({ projectId })
return data.length
}
```

## Resources on the client

Every resource is created by `createClient` and reached as `client.<resource>`.

| Resource | What it covers | Reference |
| --- | --- | --- |
| `client.workflows` | Workflows: create, update, share, export, import and run | [Workflows and projects](https://nodaro.ai/docs/developers/sdk/workflows) |
| `client.projects` | The projects that hold workflows | [Workflows and projects](https://nodaro.ai/docs/developers/sdk/workflows) |
| `client.executions` | Runs of a whole workflow | [Jobs and executions](https://nodaro.ai/docs/developers/sdk/jobs-and-executions) |
| `client.jobs` | Single generation jobs | [Jobs and executions](https://nodaro.ai/docs/developers/sdk/jobs-and-executions) |
| `client.videoPro` | Stop or continue a Generate Video Pro run | [Jobs and executions](https://nodaro.ai/docs/developers/sdk/jobs-and-executions) |
| `client.nodes` | The node catalog and single-node runs | [Run nodes](https://nodaro.ai/docs/developers/sdk/nodes) |
| `client.apps` | Published apps and their runs | [Apps and templates](https://nodaro.ai/docs/developers/sdk/apps-and-templates) |
| `client.templates` | The template marketplace | [Apps and templates](https://nodaro.ai/docs/developers/sdk/apps-and-templates) |
| `client.tutorials` | Tutorial videos and tutorial workflows | [Apps and templates](https://nodaro.ai/docs/developers/sdk/apps-and-templates) |
| `client.llm` | Structured output from a language model | [LLM and Reduce](https://nodaro.ai/docs/developers/sdk/llm-and-reduce) |
| `client.reduce` | Choose the best of many results, or combine them | [LLM and Reduce](https://nodaro.ai/docs/developers/sdk/llm-and-reduce) |
| `client.uploads` | File uploads | [Media and uploads](https://nodaro.ai/docs/developers/sdk/media-and-uploads) |
| `client.library` | Your stored media | [Media and uploads](https://nodaro.ai/docs/developers/sdk/media-and-uploads) |
| `client.media` | Download, trim, caption, overlay and collage media | [Media and uploads](https://nodaro.ai/docs/developers/sdk/media-and-uploads) |
| `client.voices` | Voices, voice changer, voice design and dubbing | [Voices and audio](https://nodaro.ai/docs/developers/sdk/voices-and-audio) |
| `client.audio` | Separate, isolate, mix, trim and transcribe audio | [Voices and audio](https://nodaro.ai/docs/developers/sdk/voices-and-audio) |
| `client.edit` | Silence detection, audio sync, edit plans and EDL renders | [Editing](https://nodaro.ai/docs/developers/sdk/editing) |
| `client.scene3d` | Editable 3D scenes and 3D Render Pro | [3D scenes](https://nodaro.ai/docs/developers/sdk/scenes-3d) |
| `client.characters` | Characters | [Characters](https://nodaro.ai/docs/developers/sdk/characters) |
| `client.locations` | Locations | [Locations](https://nodaro.ai/docs/developers/sdk/locations) |
| `client.objects` | Objects and props | [Objects and creatures](https://nodaro.ai/docs/developers/sdk/objects-and-creatures) |
| `client.creatures` | Animals and creatures | [Objects and creatures](https://nodaro.ai/docs/developers/sdk/objects-and-creatures) |
| `client.community` | The shared community library of assets | [Community library](https://nodaro.ai/docs/developers/sdk/community) |
| `client.studio` | Studio productions | [Studio productions](https://nodaro.ai/docs/developers/sdk/studio) |
| `client.shots` | Shared shot records behind share links | [Studio productions](https://nodaro.ai/docs/developers/sdk/studio) |
| `client.recast` | Recast runs and authored scripts | [Recast](https://nodaro.ai/docs/developers/sdk/recast) |
| `client.pipelines` | Story-to-video pipelines | [Pipelines](https://nodaro.ai/docs/developers/sdk/pipelines) |
| `client.copilot` | Copilot threads, inside the Nodaro app only | [Copilot](https://nodaro.ai/docs/developers/sdk/copilot) |
| `client.models` | The model catalog | [Models and credits](https://nodaro.ai/docs/developers/sdk/models-and-credits) |
| `client.credits` | Your balance and model prices | [Models and credits](https://nodaro.ai/docs/developers/sdk/models-and-credits) |
| `client.pickerCatalogs` | Valid options of each picker | [Pickers, presets and prompts](https://nodaro.ai/docs/developers/sdk/pickers-and-prompts) |
| `client.catalogs` | Every picker catalog in one call | [Pickers, presets and prompts](https://nodaro.ai/docs/developers/sdk/pickers-and-prompts) |
| `client.presets` | Saved and built-in node presets | [Pickers, presets and prompts](https://nodaro.ai/docs/developers/sdk/pickers-and-prompts) |
| `client.promptHelper` | The Prompt Wizard | [Pickers, presets and prompts](https://nodaro.ai/docs/developers/sdk/pickers-and-prompts) |
| `client.organizations` | Organizations, members and invitations | [Organizations and workspaces](https://nodaro.ai/docs/developers/sdk/organizations) |
| `client.workspaces` | Workspaces, members and join codes | [Organizations and workspaces](https://nodaro.ai/docs/developers/sdk/organizations) |
| `client.developerApps` | The OAuth apps you own | [OAuth and developer apps](https://nodaro.ai/docs/developers/sdk/developer-apps) |
| `client.oauth` | Code exchange, token revocation and consent-screen data | [OAuth and developer apps](https://nodaro.ai/docs/developers/sdk/developer-apps) |

The client itself has three more methods: [`me()`](#me), [`withWorkspace()`](#withworkspaceworkspaceid) and [`request()`](#requestmethod-path-options).

## What methods return

- **Envelopes are kept.** When an endpoint answers `{ "data": ... }`, the method resolves to that envelope, so you write `const { data } = await client.workflows.get(id)`. Paginated lists add a cursor next to `data`, such as `nextCursor`.
- **Some resources return the payload.** A few methods unwrap their response, for example `client.characters.list()` resolves to `{ characters, nextCursor }` and `client.credits.balance()` to the balance itself. Each reference page shows the exact return type.
- **Deletes and cancels** usually resolve to `{ success: true }`.
- **Field names follow the wire format.** A `Job` uses snake_case fields, such as `output_data` and `created_at`, because the API sends them that way. A `Workflow` and a `WorkflowExecution` use camelCase.

Every response and input type is exported, so you can import it with `import type`. See [Types](https://nodaro.ai/docs/developers/sdk/types).

## me()

```ts
me(): Promise<UserIdentity & MeOrganizations>
```

Returns the identity behind the current token (`GET /v1/me`). Any valid token resolves to its owner, whether it is an API token, an OAuth access token or a browser session. A missing or invalid token throws `UnauthorizedError`.

```ts
const me = await client.me()
console.log(me.email, me.tier)
```

| Field | Type | Description |
| --- | --- | --- |
| `id` | `string` | The Nodaro user id. |
| `email` | `string` | The user's email address. |
| `displayName` | `string \| null` | The display name, or `null` when it is not set. |
| `avatarUrl` | `string \| null` | The avatar URL, or `null` when it is not set. |
| `tier` | `string` | The stored subscription tier, such as `"free"` or `"pro"`. For the tier that is actually enforced, including pay-as-you-go, read `effectiveTier` from [`client.credits.balance()`](https://nodaro.ai/docs/developers/sdk/models-and-credits). |
| `isAdmin` | `boolean` | Whether the user is an administrator. Use it only to decide what to show. The server checks every permission itself. |

On a Nodaro Cloud instance with organizations, the result also carries `organizations`, `workspaces`, `lastWorkspaceId` and `organizationsUnavailable`. Treat their three states differently:

| What you see | What it means | What to do |
| --- | --- | --- |
| The fields are absent | The instance has no organizations | Do not show a workspace switcher. |
| The fields are present and empty | The account belongs to no organization | Offer to create or join one. |
| `organizationsUnavailable: true` | The lookup failed | Keep the selection you already had. Do not tell the user they lost access. |

## withWorkspace(workspaceId)

```ts
withWorkspace(workspaceId: string | null): NodaroClient
```

Returns a **new** client that acts in `workspaceId`. The new client shares the auth, base URL, timeout and fetch of the original. Pass `null` for your personal space.

```ts
const classroom = client.withWorkspace(workspaceId)

await classroom.workflows.run(workflowId) // runs in the workspace
await client.workflows.run(workflowId)    // runs in the personal space
```

The method returns a new client instead of changing the current one. Two operations that run at the same time on one client can therefore never mix up their workspaces.

The workspace decides **scope**, never **access**. It chooses which workspace a list reads from and where a new item is created. Reading, changing, deleting or running an item you name by id depends on that item's own workspace. A forgotten workspace cannot hide your work, and a wrong one cannot reach anyone else's.

Workspaces belong to organizations on Nodaro Cloud. See [Organizations and workspaces](https://nodaro.ai/docs/developers/sdk/organizations) and [Workspaces](https://nodaro.ai/docs/concepts/workspaces).

## request(method, path, options)

```ts
request<T>(method: string, path: string, options?: {
body?: unknown
query?: Record<string, string | number | boolean | undefined>
headers?: Record<string, string>
signal?: AbortSignal
}): Promise<T>
```

Sends a request to any endpoint, for the few that have no resource method yet. It adds your auth header and your workspace, sends `body` as JSON, applies `timeoutMs`, and throws the same [typed errors](https://nodaro.ai/docs/developers/sdk/errors) as the resource methods.

```ts
// The same request that client.jobs.list() sends
const page = await client.request<{ data: unknown[]; next: string | null }>("GET", "/v1/jobs", {
query: { type: "llm-structured", limit: 20 },
})
```

A `FormData` body is sent as a multipart upload. Query values that are `undefined` are left out. The [REST API reference](https://nodaro.ai/docs/developers/api) lists every endpoint and its fields.

## Timeouts and a custom fetch

`timeoutMs` aborts a request that takes longer than the limit, 60 seconds by default. Most generation takes longer than any sensible HTTP timeout, so start it and poll the job instead: [`client.nodes.runAndWait()`](https://nodaro.ai/docs/developers/sdk/nodes) does both for you. The streaming methods, `client.copilot.stream()` and `client.media.downloadVideoProgress()`, do not apply the timeout, because they are meant to stay open for minutes.

Pass your own `fetch` to change how requests travel:

- **Tests.** Return canned `Response` objects from a mock.
- **Retries.** Wrap the global `fetch` in a helper that retries on a 5xx answer.
- **Tracing.** Wrap it with your tracing or monitoring library.

```ts
const client = createClient({
baseUrl: "https://app.nodaro.ai",
auth: new StaticTokenAuth(process.env.NODARO_TOKEN!),
fetch: (input, init) => tracedFetch(input, init),
})
```

## Runtimes and browsers

The SDK uses only `fetch` and `URL`, which are global in Node.js 20 and newer, in modern browsers, in React Native, on Cloudflare Workers, on Deno and on Bun. No polyfill is needed.

- **CORS with OAuth tokens.** A browser app that calls Nodaro with an OAuth access token must run on an origin listed in the developer app's `allowedOrigins`. See [OAuth and developer apps](https://nodaro.ai/docs/developers/sdk/developer-apps).
- **CORS with a session.** A browser app that uses `supabaseAuth` is not checked against that list.
- **The client label.** On a server, the SDK sends `X-Nodaro-Client: sdk/<version>`, and Nodaro records it as the origin of each job. In a browser the default label is not sent, because the browser's `Origin` header already names your app. A `clientLabel` you set yourself is always sent.

## Frequently asked questions

### What base URL should I use with the Nodaro SDK?

Use https://app.nodaro.ai for Nodaro Cloud and the address of your instance for a self-hosted install. In a browser app served from the same origin as Nodaro, use an empty string.

### What is the default request timeout?

60 seconds. Change it with the timeoutMs option. Generation runs longer than any request, so start it and poll the job instead of raising the timeout.

### How do I make requests in a workspace?

Call client.withWorkspace(workspaceId). It returns a new client that sends every request in that workspace, and the original client keeps working in your personal space.

### How do I call an endpoint that has no SDK method?

Use client.request(method, path, options). It sends the same authentication header, applies the same timeout, and throws the same typed errors as every resource method.
