# Onei Operations API Reference

Base URL: `https://onei.ai/api/v1` · Auth: `Authorization: Bearer onei_<key>` · Envelope: `{ok, data|error}` · Rate limit: 120 req/min per key.

## Endpoints

| Method | Path | Scope | Purpose |
| ------ | ---- | ----- | ------- |
| GET | `/stats` | read | Site totals: apps (active/pending), skills, mcps, users, reviews |
| GET | `/apps` | read | List apps. Query: `status=active\|pending\|all`, `q`, `categoryId`, `page`, `perPage` |
| POST | `/apps` | write | Create app (see schema below); icon/screenshots are remote URLs sideloaded to the CDN |
| GET | `/apps/{slug}` | read | Full app detail incl. category, tags, latest reviews |
| PATCH | `/apps/{slug}` | write | Partial update; the slug never changes, even if `url` does |
| DELETE | `/apps/{slug}` | admin | Delete app |
| POST | `/apps/{slug}/approve` | write | Publish a pending submission |
| POST | `/apps/{slug}/reject` | write | Unpublish / send back to the pending queue |
| POST | `/apps/{slug}/feature` | write | Body `{featured: bool, weight?: 0-100}` |
| GET | `/apps/categories` | read | List app categories |
| POST | `/apps/categories` | admin | Create app category `{name, summary}` |
| GET | `/skills` | read | List skills. Query: `status=active\|all`, `q`, `page`, `perPage` |
| POST | `/skills` | write | Create skill (see schema below) |
| GET | `/skills/{slug}` | read | Skill detail incl. content |
| PATCH | `/skills/{slug}` | write | Partial update; include `categoryIds` to reassign categories |
| DELETE | `/skills/{slug}` | admin | Delete skill |
| POST | `/skills/{slug}/feature` | write | Body `{featured: bool}` |
| GET | `/skills/categories` | read | List skill categories |
| POST | `/skills/categories` | admin | Create `{name, slug, summary, iconName?}` |
| PATCH | `/skills/categories/{slug}` | admin | Partial update |
| DELETE | `/skills/categories/{slug}` | admin | Delete (409 if non-empty) |
| GET | `/mcps` | read | List MCP servers (same query params as skills) |
| POST | `/mcps` | write | Create MCP (see schema below) |
| GET | `/mcps/{slug}` | read | MCP detail incl. install configs |
| PATCH | `/mcps/{slug}` | write | Partial update |
| DELETE | `/mcps/{slug}` | admin | Delete MCP |
| POST | `/mcps/{slug}/feature` | write | Body `{featured: bool}` |
| GET/POST/PATCH/DELETE | `/mcps/categories[...]` | read/admin | Same shape as skill categories |

## App schema (POST /apps)

Required: `name` (2-50), `url` (unique; the slug is derived from it), `iconUrl` (remote image URL, ≤1MB, fetched and stored on the CDN), `summary` (10-200), `description` (50-5000), `categoryId` (from `GET /apps/categories`), `pricing` ("free"|"freemium"|"subscription"|"paid"), `providerName`.
Optional: `themeColor`, `previewUrl`, `screenshotUrls: string[]` (≤5 remote image URLs, ≤5MB each, sideloaded), `providerUrl`, `pending` (default false — created apps go live immediately).

## Skill schema (POST /skills)

Required: `name` (2-100), `slug` (2-100), `summary` (10-300), `description` (≥10), `author`, `content` (≥10).
Optional: `contentFormat` ("markdown"|"text"|"yaml", default markdown), `tags: string[]`, `features: string[]`, `iconUrl`, `repositoryUrl`, `documentationUrl`, `authorUrl`, `version`, `license`, `usageClaudeDesktop`, `usageCursor`, `usageWindsurf`, `usageVscode`, `usageGeneric`, `configSchema: object`, `exampleUsage`, `isActive` (default true), `isFeatured` (default false), `categoryIds: number[]`.

## MCP schema (POST /mcps)

Required: `name`, `slug`, `summary` (10-300), `description` (≥10), `author`.
Optional: `npmPackage`, `tags`, `features`, `iconUrl`, `repositoryUrl`, `documentationUrl`, `authorUrl`, `version`, `license`, `isActive`, `isFeatured`, `categoryIds`, and install configs — each `{command: string, args?: string[], env?: Record<string,string>, description?: string}`:
`installClaudeDesktop`, `installCursor`, `installWindsurf`, `installVscode`, `installCustom` (array of configs).

## Error codes

- `unauthorized` (401) — missing/invalid/revoked key
- `forbidden` (403) — key lacks the required scope
- `not_found` (404)
- `invalid_body` / `invalid_param` (422) — see `error.issues` for per-field messages
- `conflict` (409) — duplicate slug, or deleting a non-empty category
- `rate_limited` (429)
