Userz

REST API

The REST API is the public surface for widget submissions, attachments, server-side reads, and token minting.

Base URL

HTTP
https://api.userz.ai

All endpoints live under /v1. The OpenAPI 3.1 spec is served at /openapi.json and a Swagger UI is mounted at /docs on the API host (separate from this marketing site).

Authentication

Three credential types, picked by route. The widget uses the first two; backends that want to call us directly use the third.

NameTypeDescription
pub_…App public keySent as Authorization: Bearer pub_… from the widget. Identifies the App. Safe to ship to the browser. Public-mode submissions also need the App's publicSubmissionEnabled flag on.
X-Userz-User-TokenHS256 JWTPer end-user proof minted by your backend with the App's signing secret. See Private-mode tokens. Required when the App has publicSubmissionEnabled: false.
sk_…Org API keyServer-side only. Sent as Authorization: Bearer sk_…. Scoped to one Org, rate-limited per key. Plaintext is shown once in the dashboard; rotate via the API keys page.
sk_… keys and App signing secrets are bearer credentials. Anyone with one can act as your Org or impersonate any of your end-users, never embed them in client-side bundles.

Endpoints

The companion tokens guide covers private-mode JWT minting, including the SDK-free recipe for non-Node backends.

Errors

Errors are JSON: { error: { code, message, details? } }. Codes are stable strings, pattern-match on them, not on the message.

NameTypeDescription
400invalid_bodyZod validation failed. The issues array is attached on details.
401missing_authorization | invalid_authorization | unknown_app | invalid_user_tokenAuth credential missing, malformed, or rejected.
403origin_not_allowed | public_disabledOrigin not on the App's allowlist, or public-mode submission attempted on a private-only App.
413payload_too_largeBody over 8 MB or attachment over 15 MB.
429rate_limitedPer-mode rate limit exceeded. The response includes Retry-After in seconds.
5xxinternal_errorPersisted and traced. The response includes a requestId you can quote in support.

Rate limits

Token-bucket rate limiting, refilled continuously. Approximate ceilings (per minute):

NameTypeDescription
Public widgetper IP × App60 submissions / min, 120 presign / min
Private widgetper JWT sub × App120 submissions / min, 240 presign / min
Org API keyper key600 requests / min, configurable per plan

On limit, you get a 429 rate_limited with Retry-After. Treat the value as a floor, clients should add jitter and back off exponentially on repeated 429s.

Versioning

The path prefix /v1 is the version. Breaking changes ship under a new prefix; additive changes (new optional fields, new endpoints, new error codes) ship in place and are listed in the changelog.

Conventions

  • Request and response bodies are application/json unless noted.
  • Timestamps are ISO 8601 strings in UTC; durations are integer seconds unless the field name ends in Ms.
  • IDs are 24-char hex strings, except API keys and public keys which are prefixed (sk_…, pub_…).
  • Unknown fields on input are rejected (strict schema validation), typos surface as invalid_body, not silent drops.

Typed client

If your backend is in TypeScript, prefer @userz-ai/api over hand-rolled fetch. Same shape as the REST API, typed inputs + outputs + error codes, ~3.3 KB gzip, zero runtime deps. Works on Node, Bun, Deno, and Cloudflare Workers.

TypeScript
import { createUserzApi } from '@userz-ai/api';

const userz = createUserzApi({
  apiKey: process.env.USERZ_API_KEY!, // sk_...
});

// Read
const { data, nextCursor } = await userz.feedback.list({ limit: 50 });
const fb = await userz.feedback.get(data[0].id);

// Nudge a sanitized item into the agent queue
await userz.feedback.update(fb.id, { status: 'queued' });

// Mint a private-mode token over the wire (no App signing secret needed locally)
const { token } = await userz.tokens.mint({
  appId: process.env.USERZ_APP_ID!,
  body: { sub: user.id, ctx: { email: user.email } },
});