Skip to content
  • Follow System
  • English
  • 中文

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

Terminal window
npm install @picora/sdk
# or
pnpm add @picora/sdk
# or
bun add @picora/sdk

60-second example (API Key)

import { createPicoraClient } from '@picora/sdk'
const picora = createPicoraClient({
apiKey: process.env.PICORA_API_KEY!,
})
// Whoami
const me = await picora.auth.me()
console.log(`Logged in as ${me.email} (${me.plan})`)
// List your images
const { 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 automatically
const token = await flow.poll()
const picora = createPicoraClient({ oauthToken: token.accessToken })

The token object includes:

  • accessToken — pass to createPicoraClient
  • refreshToken — optional, present if your client is configured for refresh tokens
  • expiresAt — 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 0600 on 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/node uses node:fs and must not be imported in non-Node runtimes. In those environments implement the TokenStorage interface yourself (e.g. backed by localStorage, IndexedDB, or KV).

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).

Terminal window
npm install @picora/sdk keytar
# or pnpm add @picora/sdk keytar
import { 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)
  • service namespace lets you isolate prod vs staging tokens
  • account lets one machine hold tokens for multiple Picora users / OAuth clients
  • keytar is an optional peer dependency — if not installed, get()/put()/clear() throw a clear error pointing to npm install keytar. Importing KeychainTokenStorage itself does not require keytar
  • Pass backend to inject a custom adapter (e.g. @napi-rs/keyring, 1Password CLI, HashiCorp Vault):
    new KeychainTokenStorage({ backend: myCustomKeyringAdapter })

Collections + Episodes

// Create or find a collection
const series = await picora.collections.create({
name: 'My TV Show',
slug: 'my-tv-show',
collectionType: 'tv_series',
allowedResourceTypes: ['video', 'audio', 'doc'],
})
// Add an episode
const 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-After header)
  • 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:

NamespaceMethods
picora.auth / picora.userme / subscription; profile, usage, identities
picora.imagesupload / list / get / delete / batch* / exists / sign
picora.uploadsTUS 1.0 resumable: create / append / status / abort
picora.videos / picora.audio / picora.mediaupload, transcode status, unified list
picora.docs / picora.kbsMarkdown docs + knowledge-base sync
picora.collections / picora.episodeslist / get / create / update / delete; episodes.sync
startDeviceFlow(top-level helper)
MemoryTokenStorageIn-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