Typed TypeScript client for the Passband /api/v1 REST API.
The client is a thin, dependency-free wrapper over fetch. All request and
response shapes are generated from the API's OpenAPI document, so the SDK
cannot drift from the server.
npm install @passband/sdk
# or: pnpm add @passband/sdk
Requires Node.js >= 18 (uses the global fetch). Ships ESM + CJS builds and types.
import { Passband } from '@passband/sdk'
const pb = new Passband({ token: 'sf_...' })
// Run the content pipeline
await pb.pipeline.run()
// Review the ranked feed
const { drafts } = await pb.drafts.list({ status: 'pending_review' })
// Approve the first draft, if any
const first = drafts?.[0]
if (first) await pb.drafts.approve(first.id)
// Manage sources
await pb.sources.add({ name: 'HN', type: 'hackernews', url: 'https://news.ycombinator.com' })
With no options, the client reads PASSBAND_TOKEN (and PASSBAND_BASE_URL)
from the environment:
// reads PASSBAND_TOKEN from process.env
const pb = new Passband()
const summary = await pb.engagement.summary()
Constructing without a token is allowed, but the first request throws
PassbandAuthError — the SDK never sends an unauthenticated request.
Constructor options take precedence over environment variables:
| Option | Env var | Default |
|---|---|---|
token |
PASSBAND_TOKEN |
— |
baseUrl |
PASSBAND_BASE_URL |
https://passband.ai |
fetch |
— | global fetch |
retry |
— | disabled |
The token is sent as Authorization: Bearer <token>.
List endpoints are cursor-paginated. list() returns a single page (with a
nextCursor). For drafts, drafts.iterate() returns an async iterator that
transparently follows nextCursor:
for await (const draft of pb.drafts.iterate({ status: 'pending_review' })) {
console.log(draft.id)
}
Retries are opt-in. When enabled, the client retries 429 (honoring
Retry-After) and 5xx responses with exponential backoff and full jitter.
Other 4xx responses are never retried.
By default only idempotent methods are retried (GET/PUT/DELETE/PATCH).
POST is not retried, to avoid duplicating side effects. If your POST
endpoints are idempotent, opt in with retryNonIdempotent: true.
const pb = new Passband({
token: 'sf_...',
retry: { retries: 3, backoff: 250, retryNonIdempotent: false },
})
Cancellation via AbortSignal is never retried: the original AbortError
(or your signal.reason) is re-thrown unchanged.
Every non-2xx response throws a typed error: PassbandAuthError (401),
PassbandForbiddenError (403), PassbandNotFoundError (404),
PassbandRateLimitError (429, with retryAfter), and PassbandServerError
(5xx). Network and parse failures throw PassbandNetworkError. All extend
PassbandError with status, code, message, body, and requestId.
import {
Passband,
PassbandRateLimitError,
PassbandNotFoundError,
PassbandError,
} from '@passband/sdk'
const pb = new Passband({ token: 'sf_...' })
try {
await pb.drafts.approve('draft_123')
} catch (err) {
if (err instanceof PassbandRateLimitError) {
console.warn(`Rate limited; retry after ${err.retryAfter}s`)
} else if (err instanceof PassbandNotFoundError) {
console.warn('Draft not found')
} else if (err instanceof PassbandError) {
console.error(err.status, err.code, err.message, err.requestId, err.body)
} else {
throw err
}
}
| Namespace | Methods |
|---|---|
pb.pipeline |
run, status(runId), stats, runs.list |
pb.drafts |
list, iterate, get, create, update, approve, post, bulk |
pb.sources |
list, add, update, remove, test |
pb.voice |
get, update |
pb.engagement |
summary, upsert |
pb.experiments |
list, create |
pipeline.status(runId)has no dedicated API route — it performs anO(n)linear scan ofpipeline.runs.list(capped at the most recent ~2000 runs) and throwsPassbandNotFoundErrorwhen the run isn't found. Prefer pagingpipeline.runs.listdirectly when you already know the run is recent.
Runnable end-to-end examples live in examples/ — including
agent-flow.ts, the canonical loop (run pipeline → ranked feed → approve drafts →
add sources). Each reads PASSBAND_TOKEN from the environment; see
examples/README.md.
Generate the static API + REST reference (typedoc + Redocly) into docs/:
pnpm --filter @passband/sdk run docs # use `run docs` — bare `pnpm docs` is intercepted by pnpm
pnpm --filter @passband/sdk generate # regenerate openapi.json + types
pnpm --filter @passband/sdk build # tsup → ESM + CJS + d.ts
pnpm --filter @passband/sdk test # vitest (unit + OpenAPI contract)
Types are generated from app/api/v1/docs/openapi.ts (the single source of
truth, also served at /api/v1/docs). Run generate after changing the API.