Skip to main content
@stefanbrain/sdk is the official TypeScript client for the StefanBrain Developer API. It supports ESM and CommonJS, has no runtime dependencies, and requires Node.js 20.9 or later.

Install and authenticate

In CommonJS, load the client with const { StefanBrain } = require("@stefanbrain/sdk");. The constructor reads STEFANBRAIN_API_KEY. If the variable is unset and you pass no apiKey, the constructor throws AuthenticationError before it sends any request. Never expose the key in browser code or commit it to your repository. An API key uses the prepaid API wallet. An OAuth access token uses the member’s plan usage. See Pricing for the complete billing boundary.

Ask StefanBrain

client.ask completes one full StefanBrain turn. A short turn answers in the first response. When a turn takes longer than about 55 seconds, the server returns a handle instead, and client.ask polls it for up to 20 minutes.
Preserve the returned chat identifier to continue the conversation. Without chat, asks share one Developer API chat: your account’s, or the one for the project and session you pass. Pass chat: "new" to start a fresh chat. The result contains answer, chat, appUrl, runId, steps, and the server’s receipt in raw. When timeoutMs passes first, client.ask throws JobTimeoutError and the turn keeps running. Resume it by its identifier instead of asking again. Address the resume with the same chat, or the same project and session:
Since SDK 0.4.0, the error’s chat names the chat the turn runs in, so { chat: error.chat } addresses the resume too. A jobId of null means the first call never answered: retry it with the same idempotencyKey, or check the chat before asking again.

Start a run and wait

client.runs.start starts an Agent Run and returns without polling. With sync: true, StefanBrain holds the connection until the run ends, or for about 90 seconds after it receives the request. A run that ends inside that window comes back with HTTP 200, whether it completed, failed, or was cancelled. A longer run comes back as the 202 acceptance with status: "queued" and keeps running. client.runs.wait(runId, { timeoutMs, pollIntervalMs, signal }) polls a run every 3 seconds, for up to 20 minutes, until it is completed, failed, or cancelled. It returns a failed or cancelled run without throwing, so check status. At the deadline it throws JobTimeoutError, and the run keeps going. A run that already ended returns on the first poll.
client.runs.start also accepts these parameters: The SDK sets no request timeout of its own. If you bound a sync start with signal, allow at least 120 seconds.

Stream a run

client.runs.startAndStream starts a run without sync and follows its server-sent events. When the connection drops, the stream reconnects and resumes after the last event it received, using Last-Event-ID. Iteration has no deadline of its own, so bound it with a signal, or, since SDK 0.4.0, with { timeoutMs } as the second argument. Past timeoutMs, the start, the iteration, and stream.finalStatus() throw JobTimeoutError, and the run keeps going. Aborting stops the stream; it does not cancel the run.
stream.finalStatus() returns the finished run. Since SDK 0.4.0, when an idempotencyKey replays a run that has already finished, the stream yields no events and stream.finalStatus() returns that run. These methods work on any run:
  • client.runs.get(runId) reads the run. A finished run carries output and structured_output.
  • client.runs.cancel(runId) requests cancellation. It is safe to repeat.
  • client.runs.events(runId, { after }) reads one page of events. Pass the page’s next_after as after to read the next one.
  • client.runs.iterateEvents(runId) reads every page until the run ends.
Status and event reads do not count against rate limits.

Submit jobs

client.jobs.submitAndWait submits an asynchronous tool, waits for the job to finish, and returns its result.
To run each step yourself:
  • client.jobs.submit(tool, args, { chat, project, session, idempotencyKey }) returns the 202 tool_job acceptance with family, job_id, and chat.
  • client.jobs.wait(family, jobId, { chat }) polls until the job ends. It follows the server’s recommended_poll_after_ms. Without that hint, it waits 2 seconds between polls and 1.5 times longer after each one, up to 15 seconds. It returns when the job succeeds. It throws JobFailedError when the job fails or is cancelled, and JobTimeoutError after timeoutMs, 20 minutes by default.
  • client.jobs.status, client.jobs.result, and client.jobs.cancel read or cancel one job.
client.jobs.submitAndWait throws the same errors as client.jobs.wait. Job lookups are chat-scoped. Pass the acceptance’s chat, or the same project and session you submitted with. jobs.submit and jobs.submitAndWait accept create_images, edit_image, find_angles, get_meta_ad, research_shortform, review_copy, and review_funnel. An edit_image job belongs to the static_ad family. Before SDK 0.4.0, call edit_image with client.tools.invoke, which returns the same tool_job acceptance. A finished job’s result names the files it published into its chat’s workspace (kind: "files"), or carries inline data (kind: "inline"). Since SDK 0.4.0, client.jobs.downloadArtifacts finds the named files in the job chat’s listing and downloads each one. An inline result has no files, so it returns an empty list.

Call tools

Tool input and output types are generated from the tool registry when each SDK version is built. The exported DEVELOPER_SDK_SPEC_VERSION names the specification that build used. Call client.tools.list() for the live catalog, or client.tools.describe(name) for one tool, before assuming that a tool name or schema is available. A tool that returns HTTP 200 with is_error: true throws ToolResultError.

Stage and list files

client.files.upload stages local files for an ask. The returned upload_... identifiers stay valid for 24 hours.
client.files.list(chatId) lists the current workspace files in a chat. Since SDK 0.4.0, client.files.download takes a listed file, its download_url, or { chatId, path }, and returns the bytes:
A file id alone is not enough, because the download also needs the file’s chat and path. With an earlier SDK, request each entry’s download_url, a path on the API origin, with the same Authorization header. See Files for limits and error codes.

Send feedback

Since SDK 0.5.0, client.feedback tells the StefanBrain team where the API got in your way and reads the team’s replies. Reports are free.
  • client.feedback.send(params) sends one report and returns the receipt with its fb_ id and guidance. Fields use camelCase, so errorCode and requestId send error_code and request_id.
  • client.feedback.list({ status, limit }) returns your reports, newest first, with the team’s replies.
  • client.feedback.get(feedbackId) returns one report.
client.feedback.send is never retried automatically, because a retry could file the report twice. If a send fails without a response, check client.feedback.list before you send the report again. list and get are reads and retry like other reads. The SDK exports the DeveloperExternalFeedback, DeveloperExternalFeedbackReceipt, and DeveloperExternalFeedbackList types. With an earlier SDK, send the report to POST /api/developers/v1/feedback with fetch. See Agent feedback for the fields, limits, and statuses.

Retry without duplicating work

The SDK retries requests that are safe to repeat:
  • Reads, such as run status, job results, events, and the tool catalog.
  • Cancellations.
  • File staging.
  • Event-stream reconnects.
  • Submissions that carry idempotencyKey, except research_shortform.
It retries after network failures and 408, 429, 500, 502, 503, and 504 responses, at most twice by default. Set maxRetries: 0 to opt out. The SDK waits out a Retry-After of 60 seconds or less. A longer Retry-After throws at once. Without Retry-After, the first retry waits up to half a second, and each later retry waits up to twice as long. The SDK never creates an idempotency key for you. Send idempotencyKey on every run start, ask, and job submission. The SDK sends it in the Idempotency-Key header, so a retry returns the original run, turn, or job instead of starting duplicate work. A key is 1–128 characters from A–Z, a–z, 0–9, _, ., :, or -. The SDK drops an empty key. Any other key outside that format returns 400 invalid_idempotency_key. These calls deduplicate on the key for the rest of the same UTC day:
  • client.runs.start
  • client.ask
  • The asynchronous submit tools create_images, edit_image, find_angles, get_meta_ad, review_copy, and review_funnel
A reused key returns the original work even when the arguments changed. Use a new key for new work. research_shortform is not idempotent. A key does not make it safe to retry; inspect the chat before resubmitting. Pass a key only to the calls above. Other tools ignore it, but the SDK still retries them when a key is present, which can repeat a side effect such as send_email.
Keep the same chat, project, and session scope when retrying an idempotent request. Changing the scope can address a different chat and start new work.

Handle typed errors

Every deliberate SDK error extends StefanBrainError. It carries status (the HTTP status, or null when no HTTP error occurred), code (a machine-readable error code), and requestId. requestId is set only for tool-level errors, such as ToolResultError and ChatBusyError. HTTP error responses carry no request identifier. client.runs.start into a busy chat throws StefanBrainError with status 409 and code agent_run_rejected. Wait for the blocking run to finish or use a separate chat. A timeout does not mean the work failed. Resume it by its identifier instead of resubmitting it. See Parallel agent sessions for project and session isolation, or Agent Runs for the REST lifecycle.