Userz

@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

Bash
pnpm add @userz-ai/api

Zero runtime deps. Pure ESM. Works on Node 18+, Bun, Deno, and Cloudflare Workers, anywhere fetch is available.

Quick start

TypeScript
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

NameTypeDescription
apiKey*stringServer-side API key from the dashboard. Starts with sk_.
baseUrlstringOverride for self-hosted / staging. Default https://api.userz.ai.
fetchtypeof fetchCustom fetch implementation. Useful for tests, edge runtimes, or proxying.
timeoutMsnumberPer-request timeout via AbortController. Default 30000. Pass 0 to disable.
userAgentstringUser-Agent header for outgoing requests. Default @userz-ai/api/0.1.0.

Reference

NameTypeDescription
apps.list()GET /v1/appsAll Apps in the authenticated Org.
feedback.list(query?)GET /v1/feedbackCursor-paged list. Optional appId / status / cursor / limit (≤100, default 50).
feedback.get(id)GET /v1/feedback/:idFull feedback row including attachments + console logs + metadata.
feedback.update(id, body)PATCH /v1/feedback/:idSet status to queued (move sanitized → agent queue) or archived (dismiss).
agentRuns.get(id)GET /v1/agent-runs/:idPer-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.

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

TypeScript
// 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:

TypeScript
import type {
  AgentRun,
  App,
  FeedbackFull,
  FeedbackStatus,
  FeedbackSummary,
  ListFeedbackQuery,
  MintTokenBody,
  Severity,
} from '@userz-ai/api';

Custom fetch (tests + edge runtimes)

TypeScript
// 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.

Tracks the same major-version line as the API, @userz-ai/[email protected] follows /v1/*; 2.x will follow /v2/* when that lands.