JS SDK Quickstart
@picora/sdk is the official TypeScript SDK for the Picora API. Works in Node 18+, modern browsers, Cloudflare Workers, and Bun.
Install
npm install @picora/sdk# orpnpm add @picora/sdk# orbun add @picora/sdk60-second example (API Key)
import { createPicoraClient } from '@picora/sdk'
const picora = createPicoraClient({ apiKey: process.env.PICORA_API_KEY!,})
// Whoamiconst me = await picora.auth.me()console.log(`Logged in as ${me.email} (${me.plan})`)
// List your imagesconst { items } = await picora.images.list({ pageSize: 10 })console.log(`Found ${items.length} images`)OAuth Device Flow (CLI / headless)
For tools you distribute (where API Keys aren’t an option), use Device Flow:
import { startDeviceFlow, createPicoraClient } from '@picora/sdk'
const flow = await startDeviceFlow({ clientId: 'cli_xxx', scopes: ['collection.read', 'episode.write'],})
console.log(`Visit ${flow.verificationUri} and enter ${flow.userCode}`)
// Built-in RFC 8628 backoff —— handles authorization_pending / slow_down automaticallyconst token = await flow.poll()
const picora = createPicoraClient({ oauthToken: token.accessToken })The token object includes:
accessToken— pass tocreatePicoraClientrefreshToken— optional, present if your client is configured for refresh tokensexpiresAt— unix seconds (absolute, not delta)scopes— actual granted scopes (may be subset of what you asked)
Persisting the token (Node.js)
MemoryTokenStorage keeps the token only in-process. For CLI / desktop tools you usually want the token to survive restarts. Import FileTokenStorage from the Node subpath:
import { startDeviceFlow } from '@picora/sdk'import { FileTokenStorage } from '@picora/sdk/node'
const storage = new FileTokenStorage() // defaults to ~/.picora/token.json
let token = await storage.get()if (!token || token.expiresAt < Math.floor(Date.now() / 1000) + 60) { const flow = await startDeviceFlow({ clientId: 'cli_xxx', scopes: ['collection.read'] }) console.log(`Visit ${flow.verificationUri} and enter ${flow.userCode}`) token = await flow.poll() await storage.put(token)}
const picora = createPicoraClient({ oauthToken: token.accessToken })Properties:
- Default path
~/.picora/token.json; pass a custom absolute path to the constructor to override - Atomic write (tmp file + rename), so a crash mid-write won’t leave half a JSON
- POSIX
chmod 0600on the file (current user only); silently skipped on Windows - Read errors (missing file / corrupted JSON / missing fields) return
null— let the caller fall through to a fresh Device Flow
Browser / Cloudflare Workers / Bun:
@picora/sdk/nodeusesnode:fsand must not be imported in non-Node runtimes. In those environments implement theTokenStorageinterface yourself (e.g. backed bylocalStorage,IndexedDB, or KV).
OS Keychain (recommended for CLI / desktop)
Plaintext on disk is fine for dev, but production CLI tools should put refresh_token in the OS keychain (macOS Keychain / Windows Credential Manager / Linux libsecret). @picora/sdk/node ships KeychainTokenStorage with the same TokenStorage interface, backed by keytar (an optional peer dependency).
npm install @picora/sdk keytar# or pnpm add @picora/sdk keytarimport { startDeviceFlow } from '@picora/sdk'import { KeychainTokenStorage } from '@picora/sdk/node'
// service defaults to 'picora-sdk', account defaults to 'default'const storage = new KeychainTokenStorage({ account: 'cli_acme' })
let token = await storage.get()if (!token || token.expiresAt < Math.floor(Date.now() / 1000) + 60) { const flow = await startDeviceFlow({ clientId: 'cli_acme', scopes: ['collection.read'] }) console.log(`Visit ${flow.verificationUri} and enter ${flow.userCode}`) token = await flow.poll() await storage.put(token)}Properties:
- Encrypted at rest by the OS (no plaintext on disk)
servicenamespace lets you isolate prod vs staging tokensaccountlets one machine hold tokens for multiple Picora users / OAuth clientskeytaris an optional peer dependency — if not installed,get()/put()/clear()throw a clear error pointing tonpm install keytar. ImportingKeychainTokenStorageitself does not requirekeytar- Pass
backendto inject a custom adapter (e.g.@napi-rs/keyring, 1Password CLI, HashiCorp Vault):new KeychainTokenStorage({ backend: myCustomKeyringAdapter })
Collections + Episodes
// Create or find a collectionconst series = await picora.collections.create({ name: 'My TV Show', slug: 'my-tv-show', collectionType: 'tv_series', allowedResourceTypes: ['video', 'audio', 'doc'],})
// Add an episodeconst ep = await picora.episodes.create(series.id, { sequenceNo: 1, title: 'EP01 — Pilot',})
// Sync uploaded assets to the episode (idempotent + dedup)const result = await picora.episodes.sync(series.id, ep.id, { idempotencyKey: 'comfy-batch-001', assets: [ { resourceType: 'video', resourceId: 'vid_uploaded_xxx' }, { resourceType: 'doc', resourceId: 'doc_script_yyy' }, ],})
console.log(`Applied: ${result.appliedCount} / Skipped: ${result.skippedCount} / Failed: ${result.failedCount}`)For the full episode.sync contract (three-state response, source_hash dedup, audit visibility), see AI Video Sync.
Error handling
All SDK errors extend PicoraApiError:
import { PicoraApiError, PicoraRateLimitError, PicoraNetworkError } from '@picora/sdk'
try { await picora.collections.create({ name: 'X', slug: 'taken' })} catch (err) { if (err instanceof PicoraRateLimitError) { // Auto-retry happens by default; only thrown if all 3 retries exhausted console.error(`Rate limited; retry after ${err.retryAfterSec}s`) } else if (err instanceof PicoraApiError) { if (err.code === 'COLLECTION_SLUG_TAKEN') { // pick a different slug } else { throw err } } else if (err instanceof PicoraNetworkError) { // unreachable / timeout / DNS }}Retry behavior
By default the client retries:
- 429 — exponential backoff (1s / 2s / 4s, max 3 attempts; respects
Retry-Afterheader) - 5xx and network errors — 0.5s + 1.5s (max 2 attempts)
Disable: createPicoraClient({ apiKey, retryOnRateLimit: false, retryOnServerError: false })
Available namespaces (v0.3.0 — full API)
As of v0.3.0 the SDK covers 100% of the public Picora API (30+ namespaces, 228 operations) — including images.upload, videos, audio, media, docs, kbs, aigc, billing, and more. See the full namespace table in the SDK reference. Some commonly-used entries and the auth/storage helpers:
| Namespace | Methods |
|---|---|
picora.auth / picora.user | me / subscription; profile, usage, identities |
picora.images | upload / list / get / delete / batch* / exists / sign |
picora.uploads | TUS 1.0 resumable: create / append / status / abort |
picora.videos / picora.audio / picora.media | upload, transcode status, unified list |
picora.docs / picora.kbs | Markdown docs + knowledge-base sync |
picora.collections / picora.episodes | list / get / create / update / delete; episodes.sync |
startDeviceFlow | (top-level helper) |
MemoryTokenStorage | In-process token storage (cleared on exit) |
FileTokenStorage (via @picora/sdk/node) | File-backed persistence; default ~/.picora/token.json, atomic write + chmod 0600 |
KeychainTokenStorage (via @picora/sdk/node) | OS keychain persistence; requires optional keytar peer dep; service/account namespacing |
What’s next
- AI Video Sync guide — end-to-end batch flow with worked examples
- OAuth Device Flow reference — protocol details
- Python SDK — same shape, in Python