@userz-ai/api
Typed REST client for the Userz API. Server-side use with an sk_ key, the typed counterpart to hand-rolling fetch against /v1/*.
Install
pnpm add @userz-ai/apiZero runtime deps. Pure ESM. Works on Node 18+, Bun, Deno, and Cloudflare Workers, anywhere fetch is available.
Quick start
import { createUserzApi } from '@userz-ai/api';
const userz = createUserzApi({
apiKey: process.env.USERZ_API_KEY!, // sk_...
});
// List feedback for the authenticated Org
const { data, nextCursor } = await userz.feedback.list({ limit: 50 });
// Drill into one
const fb = await userz.feedback.get(data[0].id);
// Move a sanitized item into the agent queue (manual mode)
await userz.feedback.update(fb.id, { status: 'queued' });createUserzApi(options)
Options
| Name | Type | Description |
|---|---|---|
| apiKey* | string | Server-side API key from the dashboard. Starts with sk_. |
| baseUrl | string | Override for self-hosted / staging. Default https://api.userz.ai. |
| fetch | typeof fetch | Custom fetch implementation. Useful for tests, edge runtimes, or proxying. |
| timeoutMs | number | Per-request timeout via AbortController. Default 30000. Pass 0 to disable. |
| userAgent | string | User-Agent header for outgoing requests. Default @userz-ai/api/0.1.0. |
Reference
| Name | Type | Description |
|---|---|---|
| apps.list() | GET /v1/apps | All Apps in the authenticated Org. |
| feedback.list(query?) | GET /v1/feedback | Cursor-paged list. Optional appId / status / cursor / limit (≤100, default 50). |
| feedback.get(id) | GET /v1/feedback/:id | Full feedback row including attachments + console logs + metadata. |
| feedback.update(id, body) | PATCH /v1/feedback/:id | Set status to queued (move sanitized → agent queue) or archived (dismiss). |
| agentRuns.get(id) | GET /v1/agent-runs/:id | Per-run audit row: provider, model, tokens, cost, PR URL, guardrail reason. |
| tokens.mint({ appId, body }) | POST /v1/tokens/mint?appId=… | Server-side mint of a private-mode JWT. Lets your sk_ key issue tokens without holding the App signing secret in every service. |
| raw({ method, path, query?, body? }) | * | Escape hatch for endpoints not yet covered by a typed namespace. Returns the parsed JSON. |
Errors
Non-2xx responses throw UserzApiError. The HTTP status, the server's stable error code, and the response body are all on the thrown error, pattern-match on the code, not the message.
import { UserzApiError } from '@userz-ai/api';
try {
await userz.feedback.get(id);
} catch (err) {
if (err instanceof UserzApiError) {
if (err.code === 'rate_limited') {
await sleep(err.body.retryAfterMs ?? 1000);
return retry();
}
if (err.code === 'not_found') return null;
console.error(`Userz API ${err.status} ${err.code} (request id ${err.requestId})`);
}
throw err;
}The full set of stable codes is the ApiErrorCode union, see the REST API overview for the full table.
Recipe: minting widget tokens over the wire
Use this when you don't want every backend service to hold the App signing secret, they call our API instead, authenticated with an sk_ key. The Userz server signs with the secret on the way out.
// app/api/me/route.ts (Next.js)
import { NextResponse } from 'next/server';
import { createUserzApi } from '@userz-ai/api';
import { getSession } from '@/lib/auth';
const userz = createUserzApi({ apiKey: process.env.USERZ_API_KEY! });
export async function GET() {
const session = await getSession();
if (!session) return NextResponse.json({ user: null });
const { token, expiresInSeconds } = await userz.tokens.mint({
appId: process.env.USERZ_APP_ID!,
body: { sub: session.user.id, ctx: { email: session.user.email } },
});
return NextResponse.json({ user: session.user, userzToken: token, userzExpiresIn: expiresInSeconds });
}The in-process alternative is @userz-ai/node's mintUserToken(), zero network call, but every service needs the App signing secret in env.
Type definitions
Every wire shape is exported. Use them independently when modelling your own state:
import type {
AgentRun,
App,
FeedbackFull,
FeedbackStatus,
FeedbackSummary,
ListFeedbackQuery,
MintTokenBody,
Severity,
} from '@userz-ai/api';Custom fetch (tests + edge runtimes)
// Vitest example
import { createUserzApi } from '@userz-ai/api';
import { vi } from 'vitest';
const fetch = vi.fn().mockResolvedValue(
new Response(JSON.stringify({ data: [], nextCursor: null }), { status: 200 }),
);
const userz = createUserzApi({ apiKey: 'sk_test', fetch });
await userz.feedback.list();
expect(fetch).toHaveBeenCalledOnce();Cloudflare Workers and Deno expose fetch globally and don't need an override; pass one if you want to plumb in retries, custom timeouts, or KV-backed response caching.
@userz-ai/[email protected] follows /v1/*; 2.x will follow /v2/* when that lands.