Client
Create a Nodaro SDK client with createClient, set its base URL, auth, timeout and workspace, and find every resource the client exposes.
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
import { createClient, StaticTokenAuth } from "@nodaro/sdk"
const client = createClient({
baseUrl: "https://app.nodaro.ai",
auth: new StaticTokenAuth(process.env.NODARO_TOKEN!),
timeoutMs: 120_000,
})createClient(options: ClientOptions): NodaroClientProp
Type
NodaroClient is also exported as a class, so you can type a function that receives a client:
import type { NodaroClient } from "@nodaro/sdk"
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 |
client.projects | The projects that hold workflows | Workflows and projects |
client.sessions | Generation sessions inside projects | Workflows and projects |
client.executions | Runs of a whole workflow | Jobs and executions |
client.jobs | Single generation jobs, with ratings and filters | Jobs and executions |
client.webhooks | Endpoints Nodaro calls when a job ends | Jobs and executions |
client.videoPro | Stop or continue a Generate Video Pro run | Jobs and executions |
client.nodes | The node catalog and single-node runs | Run nodes |
client.apps | Published apps and their runs | Apps and templates |
client.templates | The template marketplace | Apps and templates |
client.tutorials | Tutorial videos and tutorial workflows | Apps and templates |
client.llm | Structured output from a language model | LLM and Reduce |
client.reduce | Choose the best of many results, or combine them | LLM and Reduce |
client.uploads | File uploads | Media and uploads |
client.library | Your stored media | Media and uploads |
client.gallery | The public gallery and your favorites | Media and uploads |
client.media | Download, trim, caption, overlay and collage media | Media and uploads |
client.voices | Voices, voice changer, voice design and dubbing | Voices and audio |
client.audio | Separate, isolate, mix, trim and transcribe audio | Voices and audio |
client.edit | Podcast editing in one call (tighten, chapters, trailer, clips, speaker layout), silence detection, audio sync, camera switching, edit plans and EDL renders | Editing |
client.scene3d | Editable 3D scenes and 3D Render Pro | 3D scenes |
client.characters | Characters | Characters |
client.locations | Locations | Locations |
client.objects | Objects and props | Objects and creatures |
client.creatures | Animals and creatures | Objects and creatures |
client.community | The shared community library of assets | Community library |
client.studio | Studio productions | Studio productions |
client.shots | Shared shot records behind share links | Studio productions |
client.recast | Recast runs and authored scripts | Recast |
client.castSwap | Replace the people, pets, products or place in a short video, Nodaro Cloud only | Nodaro Motion |
client.trends | Ready-made Nodaro Motion recipes run with your own pictures, Nodaro Cloud only | Trends |
client.pipelines | Story-to-video pipelines | Pipelines |
client.copilot | Copilot threads, inside the Nodaro app only | Copilot |
client.models | The model catalog | Models and credits |
client.credits | Your balance and model prices | Models and credits |
client.billing | Plans, prices and your subscription | Models and credits |
client.pickerCatalogs | Valid options of each picker | Pickers, presets and prompts |
client.catalogs | Every picker catalog in one call | Pickers, presets and prompts |
client.presets | Saved and built-in node presets | Pickers, presets and prompts |
client.palettes | Your color palettes | Pickers, presets and prompts |
client.moodboards | Your moodboards | Pickers, presets and prompts |
client.promptHelper | The Prompt Wizard | Pickers, presets and prompts |
client.organizations | Organizations, members and invitations | Organizations and workspaces |
client.workspaces | Workspaces, members and join codes | Organizations and workspaces |
client.userSettings | Your own settings: language, gallery visibility, node menu and canvas preferences, and the monthly statement email | User settings |
client.surface | What this deployment offers you: node types, models and worlds | Surface |
client.developerApps | The OAuth apps you own | OAuth and developer apps |
client.oauth | Code exchange, token revocation and consent-screen data | OAuth and developer apps |
The client itself has five more methods: me(), connectedApps(), revokeConnectedApp(), withWorkspace() and request().
Entry points per resource
The root @nodaro/sdk exports everything. Each resource is also published as its own entry point, @nodaro/sdk/<entry>, which loads only that resource and the types it takes and returns. For example, @nodaro/sdk/jobs, @nodaro/sdk/nodes and @nodaro/sdk/workflows.
@nodaro/sdk/clientholdscreateClient,NodaroClientand the auth providers.@nodaro/sdk/coreholdsNodaroClientCoreand the same auth providers.@nodaro/sdk/errorsholds the error classes andthrowFromResponse.- Every other entry is named after its resource.
@nodaro/sdk/node-presetsisclient.presets, and@nodaro/sdk/webhooksalso holdsverifyWebhookSignature.
import { NotFoundError } from "@nodaro/sdk/errors"
import type { Job, JobStatus } from "@nodaro/sdk/jobs"A value imported from an entry is the same as the one imported from the root, so instanceof NotFoundError works whichever path each side used. createClient builds every resource, so @nodaro/sdk/client loads all of them. The older subpaths @nodaro/sdk/supabase and @nodaro/sdk/signup-signals are unchanged.
A client with only the resources you use
NodaroClientCore is the client without its resources: the options createClient takes, the request methods, and me(), connectedApps() and revokeConnectedApp(). NodaroClient extends it with every resource. Extend it yourself with only the resources you call, and the other resource modules are never loaded:
import { NodaroClientCore, StaticTokenAuth } from "@nodaro/sdk/core"
import { JobsResource } from "@nodaro/sdk/jobs"
import { NodesResource } from "@nodaro/sdk/nodes"
class AppClient extends NodaroClientCore {
readonly jobs = new JobsResource(this)
readonly nodes = new NodesResource(this) // polls through `jobs`
}
const client = new AppClient({ baseUrl: "https://app.nodaro.ai", auth: new StaticTokenAuth(token) })
await client.nodes.runAndWait("generate-image", { prompt: "a lighthouse at dusk" })Every resource class takes a NodaroClientCore. NodesResource needs jobs on the same client, and Scene3DResource needs nodes: their constructor types say so, so leaving one out fails to compile. withWorkspace is a NodaroClient method. To work in a workspace, pass workspaceId to the constructor.
What methods return
- Envelopes are kept. When an endpoint answers
{ "data": ... }, the method resolves to that envelope, so you writeconst { data } = await client.workflows.get(id). Paginated lists add a cursor next todata, such asnextCursor. - Some resources return the payload. A few methods unwrap their response, for example
client.characters.list()resolves to{ characters, nextCursor }andclient.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
Jobuses snake_case fields, such asoutput_dataandcreated_at, because the API sends them that way. AWorkflowand aWorkflowExecutionuse camelCase.
Every response and input type is exported, so you can import it with import type. See Types.
me()
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.
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(). |
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. |
connectedApps()
connectedApps(): Promise<ConnectedApp[]>Returns every app with access to the signed-in user's account (GET /v1/me/connected-apps), newest grant first. The list covers developer apps, the MCP connectors of AI assistants, and self-hosted installs. It is the data behind a Connected apps settings screen. See OAuth for what a grant is.
This method needs a signed-in browser session. It accepts a session token from supabaseAuth() or createSharedSupabaseClient. It refuses an API token (ndr_…) or an app token (ndr_app_…) with UnauthorizedError, so an app cannot list or revoke the grants it uses. A server-side integration cannot call it.
const apps = await client.connectedApps()
// [{ authorizationId, name, kind, homepageUrl, scopes, connectedAt, lastUsedAt }]| Field | Type | Description |
|---|---|---|
authorizationId | string | The grant. Pass it to revokeConnectedApp(). |
name | string | null | The app's name. An MCP client that registered itself chose its own name. |
kind | DeveloperAppKind | user (a developer app), first_party_mcp, dynamic_mcp (an MCP client that registered itself) or community_instance (a self-hosted install). |
homepageUrl | string | null | The app's homepage, when it gave one. |
scopes | DeveloperAppScope[] | The scopes the grant holds. |
connectedAt | string | When the user granted access, in ISO 8601. |
lastUsedAt | string | null | The latest use of any token issued under the grant, or null if it was never used. |
revokeConnectedApp(authorizationId)
revokeConnectedApp(authorizationId: string): Promise<void>Ends an app's access (POST /v1/me/connected-apps/:authorizationId/revoke). It revokes the grant and every token issued under it at once, so the app's next call fails with 401. Like connectedApps(), it needs a signed-in browser session. It throws NotFoundError when the grant is not the user's or is already revoked.
await client.revokeConnectedApp(app.authorizationId)client.userSettings
client.userSettings reads and changes the signed-in person's own settings: the interface language, the gallery visibility, the node menu and canvas preferences, and the monthly statement email. It is also importable as @nodaro/sdk/user-settings. An app shows the settings it needs, and sends back only the fields it changes.
| Method | Endpoint | Notes |
|---|---|---|
get() | GET /v1/user/settings | Returns UserSettings: { tier, publicOutputs, promptTemplates, textTemplates, preferredLocale, mcpPreferences, showRecentNodes, showMostUsedNodes, variableDisplayMode, nodeDoubleClickAction, monthlyStatementEmail }. Throws UnauthorizedError when nobody is signed in. |
update(patch) | PATCH /v1/user/settings | Changes only the fields you send, and returns the settings as saved, without tier. |
Rules for update(patch):
promptTemplatesandtextTemplatesreplace the stored value whole, so send the full map or list. A template key the server does not know, or a value equal to the default, is dropped.mcpPreferencesis merged one axis at a time.nullclears one of its fields, for exampleupdate({ mcpPreferences: { image: { model: null } } }), and the MCP tools then use their own default for it. The patch type isUserMcpPreferencesPatch.publicOutputs: falseneeds the Standard plan or above on Nodaro Cloud. Otherwise it throwsForbiddenError.preferredLocaleis one of the languages Nodaro ships (UserSettingsLocale).- An invalid value, or a patch with nothing to change, throws a
NodaroErrorwith status 400 (validation_error). A missing profile throwsNotFoundError.
// The monthly statement toggle on a billing screen.
const { monthlyStatementEmail } = await client.userSettings.get()
await client.userSettings.update({ monthlyStatementEmail: !monthlyStatementEmail })The types are UserSettings, UserSettingsPatch, UserSettingsSaved, UserSettingsLocale, UserTextTemplate, UserMcpPreferences, UserMcpPreferencesPatch, VariableDisplayMode and NodeDoubleClickAction.
On a deployment where one billing account pays for every user, tier is the deployment's plan, the billing account's tier, and turning publicOutputs off follows that plan.
client.surface
client.surface says what this deployment offers the caller: the node types, models, Web Scrape sources and worlds an app should show the person on screen. The answer depends on who asks, so it is never shared-cached. Signed out, the answer is a user's. It is also importable as @nodaro/sdk/surface.
| Method | Endpoint | Notes |
|---|---|---|
availability() | GET /v1/surface/availability | Returns SurfaceAvailability: { nodes: { denied, hiddenFromUsers }, models: { denied, restricted }, webScrapeSources: { denied, hiddenFromUsers }, worlds }. hiddenFromUsers is empty for anyone but an administrator. models.restricted is true when the deployment limits which models are offered. A model an administrator switched off is denied, but it is not a restriction. An older server omits restricted: read that as false. worlds lists every world (SurfaceAvailabilityWorld: { id, enabled }) in the deployment's display order, and a newer server may add one. A world is enabled when the deployment lets it through and every node type it runs is available to the caller. |
// The shared header shows only the worlds this person can use.
const { worlds } = await client.surface.availability()
const tabs = worlds.filter((w) => w.enabled).map((w) => w.id)The fields are described in What this deployment offers you.
withWorkspace(workspaceId)
withWorkspace(workspaceId: string | null): NodaroClientReturns 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.
const classroom = client.withWorkspace(workspaceId)
await classroom.workflows.run(workflowId) // runs in the workspace
await client.workflows.run(workflowId) // runs in the personal spaceThe 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 and Workspaces.
request(method, path, options)
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 as the resource methods.
// 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 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() 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
Responseobjects from a mock. - Retries. Wrap the global
fetchin a helper that retries on a 5xx answer. - Tracing. Wrap it with your tracing or monitoring library.
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. - CORS with a session. A browser app that uses
supabaseAuthis 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'sOriginheader already names your app. AclientLabelyou 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.
Related
TypeScript SDK
Authentication
Errors
Organizations and workspaces
Types
Last updated on
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.
Authentication
Choose how the Nodaro SDK authenticates, with StaticTokenAuth, CallbackAuth or supabaseAuth, and share one browser login across your subdomains.