Userz

Server-side endpoints

Read Directives + agent runs, list apps, push Signals, nudge Directives through the lifecycle. Authenticate every call with an sk_ key.

These endpoints are server-only. sk_ keys can read any Directive in the Org and modify lifecycle state, never embed one in browser code or commit it to a repository.

Authentication

Every route here requires an Org API key: Authorization: Bearer sk_…. Mint keys in the dashboard under Settings → API keys. Plaintext is shown once at creation; rotate via the same page.

Per-key rate limit is approximately 600 req/min (token bucket: capacity 100, refill 10/sec). Exceeded calls return 429 rate_limited with Retry-After.

GET /v1/apps

GET/v1/appsAuth: sk_ key

List every App in the authenticated Org.

Response (200)

JSON
{
  "data": [
    {
      "id": "65fa1f3e8a1e5f2d9c1a5c01",
      "slug": "marketing-site",
      "name": "Marketing site",
      "description": "Next.js + Vercel",
      "publicKey": "pub_GLLHdYCLb2A2d9ieY2LmmwRJ",
      "publicSubmissionEnabled": true,
      "processingMode": "manual",
      "batchEnabled": false,
      "batchWindowHours": 24,
      "allowedOrigins": ["https://acme.com"],
      "createdAt": "2026-04-01T08:24:11.000Z"
    }
  ]
}

GET /v1/feedback

GET/v1/feedbackAuth: sk_ key

Cursor-paged list of Directives, newest first. All filters are optional. The path is the stable V1 wire URL; the rows it returns are Directives — the promoted unit of work.

Query

NameTypeDescription
appId24-char hex idRestrict to one App. Default: all Apps in the Org.
statusstring (DirectiveStatus)Filter on a single status value (e.g. queued, executing, pr_opened, succeeded, archived).
cursor24-char hex idThe id of the last item from the previous page. Pass back as-is to continue.
limitnumber (1–100)Default 50.

Response (200)

JSON
{
  "data": [
    {
      "id": "69e2f2d0ad2f5eef87449d83",
      "appId": "65fa1f3e8a1e5f2d9c1a5c01",
      "status": "pr_opened",
      "title": "Fix NaN cart total after coupon",
      "text": "Several users report the cart total shows NaN after applying a coupon …",
      "scale": "standard",
      "sourceKindSummary": "widget",
      "createdAt": "2026-04-18T09:11:12.000Z",
      "completedAt": null,
      "prUrls": ["https://github.com/acme/web/pull/4521"],
      "primarySignalId": "65fb0a1c8a1e5f2d9c1a5d42"
    }
  ],
  "nextCursor": "69e2f2d0ad2f5eef87449d83"
}

scale is the apparatus depth (quick / standard collapse to a single Worker; planned / epic add the Orchestrator + Validators). sourceKindSummary describes the source-kinds that drove the Directive (e.g. widget, cluster:multi), and primarySignalId points at the Signal that seeded it.

nextCursor is null when the page wasn't full, i.e. you're on the last page.

GET /v1/feedback/:id

GET/v1/feedback/:idAuth: sk_ key

Full Directive. Adds orgId, updatedAt, goal (the agent-facing goal statement), overrides, ownerBrief, and signalIds — every Signal attached to the Directive, which grows as connected signals attach. Per-submission payload (attachments, console logs, viewport, component target, submitter) lives on the underlying Signals, not the Directive.

Errors

NameTypeDescription
400invalid_idid is not a valid 24-char hex id.
404not_foundNo Directive with that id in your Org (cross-Org reads return 404, not 403).

PATCH /v1/feedback/:id

PATCH/v1/feedback/:idAuth: sk_ key

Customer lifecycle nudge. The full FSM lives server-side; this endpoint exposes two legal moves:

Body

NameTypeDescription
status"queued" | "archived"queued moves an approved / failed Directive into the agent queue (manual mode). archived dismisses from any non-terminal-success state.
ownerBriefstring (≤4000) | nullOwner-side additions to the agent prompt. null clears it. Frozen once the Directive reaches a terminal state.

Response (200)

JSON
{ "id": "69e2f2d0ad2f5eef87449d83", "status": "queued" }

Errors

NameTypeDescription
400invalid_body | invalid_id | no_opBad body, bad id, or empty patch.
404not_foundDirective id not in this Org.
409invalid_transitionCurrent status doesn't allow the requested move. Response includes from and to so the caller can render an explanation.
409concurrent_update | invalid_stateAnother writer changed the row mid-transition (retry safely), or you tried to set ownerBrief on a Directive that has already reached a terminal state.

GET /v1/agent-runs/:id

GET/v1/agent-runs/:idAuth: sk_ key

Per-run audit row. Every run leaves one of these whether it produced a PR or failed.

Response (200)

JSON
{
  "id": "65fb12cda0b8f9e2c0d33aef",
  "appId": "65fa1f3e8a1e5f2d9c1a5c01",
  "directiveId": "69e2f2d0ad2f5eef87449d83",
  "provider": "anthropic",
  "model": "claude-sonnet-4-6",
  "status": "succeeded",
  "tokensIn": 18432,
  "tokensOut": 4211,
  "costUsd": 0.231,
  "prUrl": "https://github.com/acme/web/pull/4521",
  "branch": "userz/directive-69e2f2d0",
  "guardrailReason": null,
  "error": null,
  "timings": { "prepareMs": 812, "cloneMs": 1733, "agentMs": 142091, "pushMs": 644, "prMs": 1209 },
  "startedAt": "2026-04-18T09:11:34.000Z",
  "endedAt":   "2026-04-18T09:13:48.000Z",
  "createdAt": "2026-04-18T09:11:33.000Z"
}

When status is guardrailed, guardrailReason explains why the diff was rejected before the PR opened (e.g. modified .env). When status is failed, error carries the cause.

Typed client

Hand-rolling these calls is fine, but a few lines of typed-client setup pays back fast:

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

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

const apps = await userz.apps.list();
const { data, nextCursor } = await userz.directives.list({ limit: 50 });
const directive = await userz.directives.get(data[0].id);
await userz.directives.update(directive.id, { status: 'queued' });
const run = await userz.agentRuns.get(directive.id);

Full reference at @userz-ai/api.