Puras JS/TS SDK Reference
Auto-generated reference for the puras npm package — call your deployed skills from Node, React, and React Native / Expo.
The puras npm package is the JavaScript counterpart of the
Python client: it submits jobs to your
deployed skills and hands back the result. Zero dependencies, one
fetch-based client that runs unchanged in Node (≥18),
browsers/React, and React Native / Expo.
npm install puras
It is the client half only — the in-skill runtime (puras.media,
puras.drive, …) and the deploy CLI ship with the Python package
(pip install puras).
Address a skill the same way you see it on its page — a
workspace/skillpack/skill path you can copy straight from the playground:
import { Puras } from "puras";
const puras = new Puras(); // PURAS_API_KEY from env (Node)
const ad = await puras.run("acme/ugc-ads/ugc-ad", {
product: "https://example.com/product",
duration: 15,
});
console.log(ad.video); // signed media URL
For your own skills, set a default skillpack once and call skills bare:
const puras = new Puras({ apiKey, skillpack: "ugc-ads" });
const ad = await puras.run("ugc-ad", { product: url });
For a mode: chat skill, open a conversation and .send() messages — the
thread is remembered across turns:
const chat = await puras.chat("acme/support-desk/refund-concierge");
console.log(await chat.send("Hi, my order AMZ-1001 arrived defective"));
console.log(await chat.send("It's the earbuds")); // continues the same chat
Auth is a workspace API key. In a backend, use a secret key
(puras_live_…). In a mobile/web app, use a publishable key
(puras_pub_…) — it can only submit jobs and read the jobs it submitted,
and can be fenced to specific skills and a daily spend cap when minted in
the dashboard (see API keys). Never ship a puras_live_
key inside an app bundle.
Puras
Call deployed skills. Three ways to consume a job, by increasing involvement:
- one await —
run()submits and polls to completion, returning the skill'sresult(orsubmit(…, { wait: true })for short jobs, which holds the HTTP request open server-side instead); - async —
submit()returns the job id immediately; check back later withget()/wait(); - live events (SSE) —
events()yields the job's event stream (step,tool_call,log, …) as it happens, for progress UIs.
Pass a fully-qualified workspace/skillpack/skill path to run/submit
(copyable from a skill's page), or set a default skillpack once — a slug
or UUID — and call skills by bare name.
Client is exported as an alias of Puras, matching the Python SDK's puras.Client.
new Puras(options?)
export interface PurasOptions {
/** Workspace API key (`puras_live_…` or `puras_pub_…`). Falls back to the
* `PURAS_API_KEY` env var when running under Node. */
apiKey?: string;
/** Default skillpack (slug or UUID) for bare skill names. */
skillpack?: string;
/** API origin; defaults to https://api.puras.co (or `PURAS_API_BASE`). */
apiBase?: string;
/** Per-request timeout in ms for non-streaming calls. Default 120 000. */
timeoutMs?: number;
/** Custom fetch implementation (tests, polyfills). Defaults to global fetch. */
fetch?: typeof fetch;
}
puras.submit(skill, inputs?, options?)
async submit(
skill: string,
inputs: Record<string, unknown> = {},
options: SubmitOptions = {},
): Promise<Job>
Submit a job and return the job object ({id, status, …}) immediately —
or, with wait: true, after it finishes (server-side wait, bounded by
timeoutSeconds).
skill may be a fully-qualified path — "workspace/skillpack/skill"
(copyable from the skill's page) or "skillpack/skill" for one of your
own — in which case the skillpack is taken from the path. A bare skill
name uses the default skillpack passed here or to new Puras(...).
puras.run(skill, inputs?, options?)
async run(
skill: string,
inputs: Record<string, unknown> = {},
options: RunOptions = {},
): Promise<Record<string, unknown>>
Submit + wait, returning the skill's result object. Throws JobError
(carrying the full job) if the job failed, was cancelled, or is still
running when timeoutMs (default 10 minutes) elapses.
Waiting is client-side polling (pollIntervalMs, default 2 s) rather
than a held connection — media skills run for minutes, longer than any
proxy keeps a request open, and React Native's fetch can't stream.
puras.get(jobId)
async get(jobId: string): Promise<Job>
Fetch a job by id (status, result, error, outputs with signed URLs).
puras.feedback(jobId, options?)
async feedback(jobId: string, options: FeedbackOptions = {}): Promise<JobFeedback>
Record end-user feedback on a job's result — a thumb (rating) and/or a
free-form comment — so you can later see which skills run well and which
don't. The natural flow is run → show the result → feedback:
const result = await puras.run("acme/ugc-ads/ugc-ad", { product: url });
// …user reacts in your UI…
await puras.feedback(job.id, { rating: 1, endUserId: user.id });
At least one of a non-zero rating or a comment is required. Works with
either key kind; a publishable key can collect feedback straight from a
web/mobile app, scoped like jobs to the ones it submitted.
puras.wait(jobId, options?)
async wait(jobId: string, options: WaitOptions = {}): Promise<Job>
Poll a job until it reaches a terminal status (succeeded / failed /
cancelled) or timeoutMs elapses; returns the last-seen job either way.
puras.events(jobId, options?)
async *events(jobId: string, options: EventsOptions = {}): AsyncGenerator<JobEvent>
Stream a job's events live — an async iterator that yields each
JobEvent as the worker emits it and ends when the job reaches a
terminal status:
const job = await puras.submit("acme/ugc-ads/ugc-ad", { product: url });
for await (const ev of puras.events(job.id)) {
console.log(ev.type, ev.payload); // step, tool_call, log, …
}
const done = await puras.get(job.id); // terminal by now
Transport is picked automatically: Server-Sent Events over a streaming
fetch (GET /v1/jobs/{id}/stream) where the runtime supports it
(Node ≥18, browsers), falling back to polling
GET /v1/jobs/{id}/events on runtimes that can't stream (React
Native / Expo). Force one with transport: "sse" | "poll". The stream
replays the job's full event backlog first, so a viewer attached late
still sees everything (resume with afterId).
puras.chat(skill, options?)
async chat(skill: string, options: ChatOptions = {}): Promise<Conversation>
Open a multi-turn chat against a mode: chat skill and return a
{@link Conversation} you can .send() messages to. The skill's SKILL.md is
the system prompt and its tools are the agent's tools; each message runs one
turn (a job) and resolves with the model's free-form reply.
const chat = await puras.chat("acme/support-desk/refund-concierge");
console.log(await chat.send("Hi, my order AMZ-1001 arrived defective"));
console.log(await chat.send("It's the earbuds, EARBUDS-X2")); // remembers the thread
Works with either key kind. A publishable key (puras_pub_…) can open
conversations and send turns straight from a web/mobile app — scoped, like
jobs, to the conversations it created and bounded by its allowed-skills fence
and daily spend cap; a secret key (puras_live_…) is unrestricted in a
backend.
puras.createConversation(skill, options?)
async createConversation(
skill: string,
options: ChatOptions = {},
): Promise<ConversationData>
Create a conversation row (lower-level; most callers want {@link chat}).
puras.getConversation(conversationId)
async getConversation(conversationId: string): Promise<ConversationData>
Fetch a conversation by id, including its rendered transcript (messages).
puras.listConversations(options?)
async listConversations(
options: { skillpackId?: string; limit?: number } = {},
): Promise<ConversationData[]>
List your conversations, most recently updated first.
puras.sendMessage(conversationId, message)
async sendMessage(conversationId: string, message: string): Promise<Job>
Send one message and return the turn's job immediately (before the model
replies). Poll/stream it like any job — wait(job.id) then read
job.result.reply, or events(job.id) for live tool activity. Most callers
want {@link Conversation.send} instead.
puras.deleteConversation(conversationId)
async deleteConversation(conversationId: string): Promise<void>
Delete a conversation.
Types
/** A job as returned by the API. Extra fields ride along untyped. */
export interface Job {
id: string;
status: string;
result?: Record<string, unknown> | null;
error?: string | null;
skill_name?: string;
created_at?: string;
[key: string]: unknown;
}
/** One job event (`step`, `tool_call`, `log`, …) as emitted by the worker. */
export interface JobEvent {
/** Durable events carry the job_events row id; EPHEMERAL events (live-only,
* e.g. `text_delta` — the model's text as it streams) have `id: null`. They
* appear only on the SSE transport, never in the `/events` backlog, so an
* `afterId` resume replays none of them; the full turn text is on the
* durable `model_response` event's `payload.text`. */
id: number | null;
type: string;
payload: Record<string, unknown> | null;
[key: string]: unknown;
}
export interface SubmitOptions {
/** Skillpack override (slug, `workspace/skillpack` path, or UUID). */
skillpack?: string;
/** Pin the run to a specific deployment version; omit for the active one. */
version?: number;
/** Block server-side until the job finishes or `timeoutSeconds` elapses
* (synchronous mode — the HTTP request itself waits). Short jobs only;
* for multi-minute media jobs prefer `run()` or `wait()`. */
wait?: boolean;
/** Server-side wait budget in seconds (only with `wait: true`). Default 60. */
timeoutSeconds?: number;
}
export interface WaitOptions {
/** Give up after this long. Default 600 000 (10 min — media jobs take minutes). */
timeoutMs?: number;
/** Poll interval. Default 2000. */
pollIntervalMs?: number;
}
export interface EventsOptions {
/** Yield only events with id greater than this (resume a dropped stream). */
afterId?: number;
/** `"sse"` = live Server-Sent Events over a streaming fetch; `"poll"` =
* repeated `GET /v1/jobs/{id}/events` (works everywhere, incl. React
* Native, whose fetch can't stream). Default `"auto"`: SSE when the
* runtime supports response streaming, else poll. */
transport?: "auto" | "sse" | "poll";
/** Poll interval for the poll transport. Default 1500. */
pollIntervalMs?: number;
/** Abort the stream early (e.g. when a screen unmounts). */
signal?: AbortSignal;
}
/** A thumb: `+1` good, `-1` bad, `0` comment-only. */
export type FeedbackRating = -1 | 0 | 1;
/** Wait/poll budget for a single chat turn (same shape as {@link WaitOptions}). */
export type SendOptions = WaitOptions;
export type RunOptions = Pick<SubmitOptions, "skillpack" | "version"> & WaitOptions;
Errors
PurasAPIError
Non-2xx response from the Puras API. Notable cases: 402 insufficient
credits, 403 skill not in the key's allowlist, 429 the key's daily
spend cap is reached.
JobError
A job finished in a non-succeeded state (failed / cancelled / still
running at timeout). .job holds the full job object.