Using @nodaro/sdk

@nodaro/sdk is the typed REST SDK for Nodaro. Use it instead of hand-rolling fetch calls.

Install

npm install @nodaro/sdk

(Currently not yet on public npm — will land once the project’s licensing is finalized. Internal users: install from a local workspace.)

Three auth modes — pick by environment

Server-side (most common)

import { createClient, StaticTokenAuth } from "@nodaro/sdk"

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

The token can be:

Browser app (your operator’s frontend)

import { createClient, supabaseAuth } from "@nodaro/sdk"
import { createClient as supa } from "@supabase/supabase-js"

const supabase = supa(SUPABASE_URL, SUPABASE_ANON_KEY)
const client = createClient({
  baseUrl: import.meta.env.VITE_API_URL ?? "",  // empty string = same-origin via proxy
  auth: supabaseAuth(supabase),
})

The Supabase session JWT gets sent on every request. If session is null, no auth header.

Custom auth logic (refresh tokens, etc.)

import { createClient, CallbackAuth } from "@nodaro/sdk"

const client = createClient({
  baseUrl: "https://nodaro.example.com",
  auth: new CallbackAuth(async () => {
    return await refreshTokenIfExpired()  // your logic
  }),
})

17 resources

client.workflows       // list, get, getPublic, create, update, delete, run, export, import
client.projects        // list, get, create, update, delete
client.jobs            // get, getStatus, cancel
client.executions      // get, listForWorkflow, cancel
client.nodes           // list, get, run, runAndWait, runMany
client.characters      // list, get, create, update, upsert, delete, restore, duplicate, usage, generate, generateAsset, generateMotion, approvePortrait, recaption
client.locations       // list, listArchived, get, create, update, delete, restore, generate, generateAsset, generateMotion, approveMainImage, recaption
client.objects         // list, listArchived, get, create, update, delete, restore, permanentDelete, generate, generateAsset, generateMotion, approveMainImage, recaption
client.pipelines       // create, get, list, cancel, pendingApprovals, approveStage, rejectStage, approveSubGate, getStage, getTimeline, branch, chatStage, applyChatProposal, getStageChat
client.reduce          // run
client.promptHelper    // analyze, generate, enhance
client.apps            // list, get, run, listRuns, getRun, deleteRun
client.developerApps   // list, get, create, update, delete, rotateSecret
client.oauth           // exchangeCode, revoke, getAppInfo
client.voices          // list, searchLibrary, listClones, createClone, deleteClone, change
client.credits         // balance, modelCosts
client.uploads         // upload

Method signatures match the underlying /v1/* REST routes. All return Promise<{ data: T }> or Promise<T> per the route’s response envelope.

Typed error hierarchy

import {
  NodaroError,
  UnauthorizedError,
  ForbiddenError,
  NotFoundError,
  RateLimitedError,
  InsufficientCreditsError,
  StorageExceededError,
} from "@nodaro/sdk"

try {
  await client.workflows.run(id)
} catch (err) {
  if (err instanceof UnauthorizedError) {
    // 401 — re-auth
  } else if (err instanceof ForbiddenError && err.missingScope) {
    // 403 with insufficient_scope — re-prompt for additional scope
  } else if (err instanceof InsufficientCreditsError) {
    // 402 — surface required vs available
    console.log(`Need ${err.required} credits, have ${err.available}`)
  } else if (err instanceof RateLimitedError) {
    // 429 — backoff
  } else if (err instanceof NodaroError) {
    // catch-all — has .code and .status
    console.log(err.code, err.status)
  }
}

Common recipes

Run a workflow + poll for completion

const exec = await client.workflows.run(workflowId, { nodeIds: ["node-id"] })

while (true) {
  const { data } = await client.executions.get(exec.executionId)
  if (data.status === "completed" || data.status === "failed") break
  await new Promise(r => setTimeout(r, 2000))
}

Discover what nodes are available

const { data: nodes } = await client.nodes.list()
const imageGenNodes = nodes.filter(n => n.category === "ai-image")

Run inline with ?wait=true (no polling)

This is exposed via client.workflows.run if the param is supported — check the SDK source. Otherwise fall back to direct client.request("POST", "/v1/workflows/" + id + "/run?wait=true&timeout=120", ...) — the underlying client exposes a generic request method.

Reasoning effort (LLM-backed nodes)

LLM-backed feature routes accept an optional reasoningEffort field in the request body — "none" | "low" | "medium" | "high" | "xhigh" | "max", model-dependent (see the model table in the model selector section of the Generate Text node docs). Omit it for the vendor default (“Auto”). xhigh and max bill one tier up (economy → standard, standard → premium); see the Reasoning effort section for the exact rule. Workflow/canvas LLM nodes carry the same field on their node data, and client.promptHelper.* accepts it directly in its request body.

client.nodes.run(type, params) POSTs params straight to POST /v1/<type> — that matches the registered route only for generate-script, image-critic, qa-check, and describe-to-picker. Other LLM-backed node types register at a nested path instead (llm-chat/v1/llm-chat/generate, after-effects/v1/after-effects/generate, motion-graphics/v1/motion-graphics/generate, lottie-overlay/v1/lottie-overlay/generate, 3d-title/v1/3d-title/generate, image-to-text/v1/image-to-text/describe, video-composer/v1/scene-graph/generate) — use client.request("POST", "<path>", { body }) for those, or client.promptHelper.*, which always posts to /v1/prompt-helper/wizard regardless of node type.

// Bare-path node type — client.nodes.run() posts directly to /v1/generate-script.
await client.nodes.run("generate-script", {
  prompt: "Draft a 3-scene product launch script for a smart water bottle.",
  llmModel: "claude-sonnet-5",
  reasoningEffort: "high",
})

The CLI exposes the same lever as --reasoning-effort <level> on nodaro prompt wizard/analyze/generate/enhance, and as --param reasoningEffort=<level> on nodaro nodes run <type>.

When NOT to use the SDK

For all other cases — including single-node single-shot routes — the SDK is the right tool. client.nodes.run(type, params) calls POST /v1/<type> directly without needing a workflow, and client.nodes.runAndWait(type, params) polls to completion for you:

// Run a single node directly — no workflow needed
const output = await client.nodes.runAndWait("generate-image", {
  prompt: "a snow leopard in the mountains",
  provider: "recraft",
})
console.log(output.imageUrl)

Custom fetch / timeout

const client = createClient({
  baseUrl: ...,
  auth: ...,
  fetch: customFetch,       // for retries, OpenTelemetry, tests
  timeoutMs: 120_000,        // default 60_000
})

TypeScript types

The SDK exports types alongside resources. Common ones:

import type {
  Workflow, Project, Job, WorkflowExecution, NodeDescriptor,
  DeveloperApp, ExchangeCodeInput, AccessTokenResponse,
} from "@nodaro/sdk"

These match the wire shapes (e.g., Job uses snake_case created_at because that’s what sanitizeJobForPublic returns; Workflow and WorkflowExecution use camelCase because their backend handlers map it).

Reference