Get started
TypeScript SDK
@upvoiced/sdk is a thin, typed client over fetch. Every path, parameter, body and response type is generated from the same OpenAPI document as this reference, so the paths are the API’s own (/v1/posts/{id}) and the reference’s SDK examples map one to one. No runtime dependencies; Node 22+, Deno, Bun and Cloudflare Workers.
npm install @upvoiced/sdkQuick start
// UpVoiced API with @upvoiced/sdk (Node 22+, Deno, Bun, Cloudflare Workers). Server-side only:
// API keys are secrets.
import { UpVoiced } from '@upvoiced/sdk';
const apiKey = process.env['UPVOICED_API_KEY']; // uv_live_… (Settings → API keys)
const workspace = process.env['UPVOICED_WORKSPACE_ID']; // sent as X-Workspace
if (!apiKey || !workspace) throw new Error('Set UPVOICED_API_KEY and UPVOICED_WORKSPACE_ID.');
const upvoiced = new UpVoiced({ apiKey, workspace });
const { items: boards } = await upvoiced.get('/v1/boards');
const board = boards[0];
if (board) {
const post = await upvoiced.post('/v1/posts', {
body: { board_id: board.id, title: 'Dark mode', body_md: 'Asked for by the night shift.' },
});
await upvoiced.patch('/v1/posts/{id}', { path: { id: post.id }, body: { title: 'Dark theme' } });
}
Your editor completes paths, query parameters and bodies, and TypeScript rejects a missing path parameter, an unknown field or a wrong enum value. The client covers all205 operations of the API reference.
Client options
| Option | Default | Meaning |
|---|---|---|
apiKey | — | API key (uv_live_…), sent as Authorization: Bearer. Omit for public reads. |
workspace | — | Workspace id (X-Workspace); a workspace slug for /v1/public/*. |
baseUrl | https://api.upvoiced.com | API origin. |
maxRetries | 2 | Retries after 429, 502/503/504 and network errors. |
maxRetryDelayMs | 60000 | A longer Retry-After is not waited for: the error is thrown. |
fetch | global fetch | Custom fetch (proxies, tests). |
headers | — | Extra headers on every request. |
Every call also accepts workspace, idempotencyKey, headers and an AbortSignal (signal). An operator-wide key can act on another workspace with upvoiced.withWorkspace(id).
Retries and idempotency
The client retries a 429 for any method (the request did not run), and a 502, 503, 504 or network error for GET, PUT, DELETE and POST. Every POST carries a generatedIdempotency-Key that is reused on each retry, so a retried POST cannot apply twice. PATCH is never retried after a 5xx. It waits for Retry-After when present, otherwise an exponential backoff with jitter.
Pagination
import { UpVoiced } from '@upvoiced/sdk';
const apiKey = process.env['UPVOICED_API_KEY']; // uv_live_… (Settings → API keys)
const workspace = process.env['UPVOICED_WORKSPACE_ID']; // sent as X-Workspace
if (!apiKey || !workspace) throw new Error('Set UPVOICED_API_KEY and UPVOICED_WORKSPACE_ID.');
const upvoiced = new UpVoiced({ apiKey, workspace });
// Every item, fetching pages of 100 lazily; stop iterating to stop fetching.
for await (const post of upvoiced.paginate('/v1/posts', { query: { sort: 'top', limit: 100 } })) {
console.log(post.title, post.vote_count);
}
// Or one page at a time: pass next_cursor back as cursor until it is null.
let cursor: string | undefined;
do {
const page = await upvoiced.get('/v1/posts', {
query: { limit: 100, ...(cursor ? { cursor } : {}) },
});
for (const post of page.items) console.log(post.title);
cursor = page.next_cursor ?? undefined;
} while (cursor);
Errors
import { UpVoiced, UpVoicedError } from '@upvoiced/sdk';
const apiKey = process.env['UPVOICED_API_KEY']; // uv_live_… (Settings → API keys)
const workspace = process.env['UPVOICED_WORKSPACE_ID']; // sent as X-Workspace
if (!apiKey || !workspace) throw new Error('Set UPVOICED_API_KEY and UPVOICED_WORKSPACE_ID.');
const upvoiced = new UpVoiced({ apiKey, workspace });
try {
await upvoiced.post('/v1/posts', {
body: { board_id: '0190f5e2-4c1d-7a3b-8c2d-5e6f7a8b9c0d', title: '' },
});
} catch (error) {
if (!(error instanceof UpVoicedError)) throw error;
console.log(error.status); // 400
console.log(error.code); // 'invalid_request'
console.log(error.details); // [{ path: 'title', code: 'too_small' }]
console.log(error.retryAfter); // seconds, on 429 and 503
}
Types
import type { Operation, RequestBody, ResponseData } from '@upvoiced/sdk';
type Post = ResponseData<Operation<'/v1/posts/{id}', 'get'>>;
type NewPost = RequestBody<Operation<'/v1/posts', 'post'>>;