Skip to content
UpVoiced Docs

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/sdk

Quick 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

new UpVoiced(options)
OptionDefaultMeaning
apiKey—API key (uv_live_…), sent as Authorization: Bearer. Omit for public reads.
workspace—Workspace id (X-Workspace); a workspace slug for /v1/public/*.
baseUrlhttps://api.upvoiced.comAPI origin.
maxRetries2Retries after 429, 502/503/504 and network errors.
maxRetryDelayMs60000A longer Retry-After is not waited for: the error is thrown.
fetchglobal fetchCustom 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'>>;