REST API
The REST API is the public surface for widget submissions, attachments, server-side reads, and token minting.
Base URL
https://api.userz.aiAll 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.
| Name | Type | Description |
|---|---|---|
| pub_… | App public key | Sent 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-Token | HS256 JWT | Per 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 key | Server-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
POST /v1/feedback: submit a feedback report (text + console logs + viewport + attachment refs).POST /v1/feedback/upload-url: request a presigned PUT URL for a screenshot or attachment, then reference the returned key on the subsequent submission.GET /v1/feedback,GET /v1/feedback/:id,PATCH /v1/feedback/:id,GET /v1/apps,GET /v1/agent-runs/:id: server-side reads + lifecycle nudges for the authenticated Org. Require ansk_…key.POST /v1/tokens/mint: server-side mint of a private-mode JWT. Lets yoursk_key issue tokens without holding the App signing secret in every backend service.
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.
| Name | Type | Description |
|---|---|---|
| 400 | invalid_body | Zod validation failed. The issues array is attached on details. |
| 401 | missing_authorization | invalid_authorization | unknown_app | invalid_user_token | Auth credential missing, malformed, or rejected. |
| 403 | origin_not_allowed | public_disabled | Origin not on the App's allowlist, or public-mode submission attempted on a private-only App. |
| 413 | payload_too_large | Body over 8 MB or attachment over 15 MB. |
| 429 | rate_limited | Per-mode rate limit exceeded. The response includes Retry-After in seconds. |
| 5xx | internal_error | Persisted and traced. The response includes a requestId you can quote in support. |
Rate limits
Token-bucket rate limiting, refilled continuously. Approximate ceilings (per minute):
| Name | Type | Description |
|---|---|---|
| Public widget | per IP × App | 60 submissions / min, 120 presign / min |
| Private widget | per JWT sub × App | 120 submissions / min, 240 presign / min |
| Org API key | per key | 600 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/jsonunless 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.
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 } },
});