# SkillSafe — Full API Reference > The secured skill registry for AI agents. Scan, save, share, install, and verify AI skills with dual-side cryptographic verification. SkillSafe provides a hosted registry for AI agent skills (Claude Code, Cursor, Windsurf, Codex, and 30+ other tools). End users install skills via the AI SkillSafe desktop app or Vercel's `skills` CLI; tools and integrators consume skills via REST and a legacy MCP endpoint. ## Quick Start ```bash # Recommended (CLI users): every SkillSafe skill is cloneable via git, # so Vercel's skills CLI works natively. Anonymous — public/shared skills only. npx skills add https://api.skillsafe.ai/{namespace}/{name} # Or install the Node.js CLI globally: npm install -g @skillsafe/cli skillsafe login # sign in — required to install a PRIVATE skill you own skillsafe add @namespace/skill-name # also: install, i (sends your credential when signed in) skillsafe list # installed skills, from skillsafe.lock skillsafe update # re-resolve every skill to its latest version skillsafe audit # re-check scan verdicts # The CLI is a convenience wrapper. Every one of its actions is a plain HTTP # call documented below, so an agent that cannot install an npm package has a # complete path: resolve the skill, read its file manifest, fetch each blob. # Recommended (end users): the AI SkillSafe desktop app # https://app.skillsafe.ai/ — macOS / Windows / Linux # Click "View in AI SkillSafe app" on any skill page. ``` ## Resources - [Desktop app](https://app.skillsafe.ai/): AI SkillSafe for macOS / Windows / Linux. Recommended install path. Opens via `skillsafe://install?ns=...&name=...&version=...` deep link from any skill page. - [`@skillsafe/cli` on npm](https://www.npmjs.com/package/@skillsafe/cli): `npm install -g @skillsafe/cli`. Source: see project docs. - [`npx skills add`](https://github.com/vercel-labs/skills): Vercel's skills CLI; works natively because every SkillSafe skill is cloneable via git smart-HTTP at `https://api.skillsafe.ai/{ns}/{name}`. - [MCP Server](https://api.skillsafe.ai/mcp) (legacy / programmatic): JSON-RPC MCP endpoint kept for backwards compatibility. Tools: `search_skills`, `recommend_skills`, `get_skill_info`, `install_skill`, `scan_skill`, `scan_mcp_config`, `save_skill`, `share_skill`. - [OpenAPI Spec](https://skillsafe.ai/openapi.json): Machine-readable OpenAPI 3.1 spec - [Documentation](https://skillsafe.ai/docs): Full user docs - [Security Model](https://skillsafe.ai/security): Scanner rules and verification flow - [Skill Markdown](https://skillsafe.ai/skill/@{ns}/{name}/?md): Plain-text markdown summary of any public skill (e.g. `?md` query param on any skill detail page) - [Blog Post Markdown](https://skillsafe.ai/blog/{slug}.md): Raw markdown source of any blog post (append `.md` to the post URL) --- ## API Reference **Base URL:** `https://api.skillsafe.ai` **Authentication:** `Authorization: Bearer ` or `ss_session` HttpOnly cookie. **Response envelope:** ```json { "ok": true, "data": { ... }, "meta": { "request_id": "...", "pagination": { ... } } } ``` **Error format:** ```json { "ok": false, "error": { "code": "not_found", "message": "Skill not found" } } ``` **Error codes:** `unauthorized`, `forbidden`, `not_found`, `conflict`, `validation_error`, `invalid_request`, `rate_limited`, `email_not_verified`, `scan_required`, `storage_limit_exceeded`, `key_limit_exceeded`, `internal_error` **Rate limits** (per IP, sliding window): | Endpoint pattern | Limit | |---|---| | `/v1/auth/*` | 10 req/min | | Verify, GitHub import | 10 req/min | | `/v1/billing/*` | 30 req/min | | Search, share, save | 60 req/min | | `/v1/creator/*` | 60 req/min | | `/v1/creator/chat` | 10 req/min | | All other `/v1/*` | 120 req/min | Returns `Retry-After` header and 429 status when exceeded. --- ### Platform #### `GET /v1/health` Live system check. No auth required. ```json { "ok": true, "data": { "status": "ok", "db": "ok", "storage": "ok" } } ``` #### `GET /v1/stats` Public aggregate platform metrics. ```json { "ok": true, "data": { "skills": 1200, "publishers": 340, "scan_reports": 4800 } } ``` #### `GET /v1/cli/version` Latest CLI version info. --- ### Search #### `GET /v1/skills/search` Search public skills. No auth required. **Query params:** - `q` — search query (FTS5 full-text search) - `category` — filter by category slug - `namespace` — filter to a specific namespace - `type` — `skill` or `skillset` - `sort` — `popular` | `recent` | `verified` | `trending` | `hot` (default: `recent`) - `limit` — 1–100 (default: 20) - `cursor` — cursor-based pagination - `page` — page number (overrides cursor, max 200) **Response** includes `meta.pagination` with `has_more`, `next_cursor`, `total_count`, `page`, `per_page`, `total_pages`. ```json { "ok": true, "data": [ { "skill_id": "skl_abc123", "namespace": "alice", "name": "code-review", "description": "AI-powered code review skill", "category": "development", "tags": ["review", "quality"], "current_version": "1.2.0", "download_count": 340, "star_count": 28, "verification_count": 156, "scan_grade": "A" } ], "meta": { "request_id": "...", "pagination": { "has_more": false, "next_cursor": null, "total_count": 1 } } } ``` --- ### Skills #### `GET /v1/skills/@{ns}/{name}` Get skill metadata. Private skills return 404 to non-owners. #### `POST /v1/skills/@{ns}/{name}` — Auth required Save a new skill version. Skills are **private by default**. **Request:** `multipart/form-data` - `archive` (required): ZIP file of the skill directory - `metadata` (optional): JSON string ```json { "version": "1.0.0", "description": "...", "category": "development", "tags": ["tag1", "tag2"], "changelog": "[patch] fixed command fallback" } ``` If `version` is omitted, the patch version is auto-incremented. If content is unchanged, the save is skipped (returns 200 with `skipped: true`). **Tiers:** - Free: unlimited private saves and unlimited shared skills; 50 MB storage - Pro/Enterprise: more storage (10 GB on Pro); share counts are unlimited on every tier #### `DELETE /v1/skills/@{ns}/{name}` — Auth required Soft-deletes the skill and all versions. #### `GET /v1/skills/@{ns}/{name}/app` — No auth The public hosted app built from this skill, or `{"app": null}`. Returns `{"app": {"slug", "title", "description", "url"}}`. Use it to offer the no-install path: anyone without an agent can open that URL in a browser and use the skill directly. Public + active apps only — an unlisted or private app is never advertised here. #### `POST /v1/skills/batch/versions` — No auth Resolve the latest version of up to 200 skills in one request. Body `{ "skills": ["@ns/name", ...] }` → `{ "versions": { "@ns/name": "1.2.0" } }`. Use this instead of one metadata GET per skill when reconciling a set of installs. #### `POST /v1/scan/files` — No auth Run the platform scanner over files you hold locally, without saving anything. Body `{ "files": [{ "path": "SKILL.md", "content": "...", "size": 123 }] }` (max 1000 files, 2 MB per file, 20 MB total; 30 req/min). Returns the scan result: `raw_findings` (already post-filtered for known false positives, each with `severity` of `critical`/`high`/`medium`/`low`/`info`), `bom`, `file_count`, and the `scanner` version pair. This is the report to attach as `scan_report` when saving — the server owns the ruleset and the false-positive filter, so a locally-invented report would drift from the gates that later read it. Note the scanner does not extract code from markdown fences: a pattern only trips a rule when it lives in a real code file. #### `GET /v1/skills/@{ns}/{name}/versions` Paginated version list. Params: `limit`, `cursor`. #### `GET /v1/skills/@{ns}/{name}/versions/{version}` Version details including `tree_hash` and scan grade. #### `GET /v1/skills/@{ns}/{name}/download/{version}` — No auth for public skills Returns the version's file manifest as JSON, not an archive: `{ "format": "files", "tree_hash": "...", "files": [{ "path": "SKILL.md", "hash": "sha256:...", "size": 1234 }] }`. Fetch each file's bytes from `GET /v1/blobs/{hash}`. Auth is required only for a private skill, and then only for its owner. #### `GET /v1/blobs/{hash}` — No auth for public skills Raw bytes of one content-addressed file, keyed by the `sha256:...` hash from a download manifest. This plus the endpoint above is the complete raw-HTTP install path: resolve the skill, read its manifest, fetch each blob, write it to your tool's skills directory. Verify each blob against its hash — that is what makes the install tamper-evident. #### `POST /v1/skills/@{ns}/{name}/versions/{version}/yank` — Auth required Mark a version as yanked (visible but not downloadable). ```json { "reason": "security issue in v1.0.0" } ``` #### `POST /v1/skills/@{ns}/{name}/star` — Auth required Star a skill. #### `DELETE /v1/skills/@{ns}/{name}/star` — Auth required Unstar a skill. #### `POST /v1/skills/@{ns}/{name}/current-version` — Auth required Set the default version shown on the skill page. ```json { "version": "1.2.0" } ``` --- ### Share Links Share links allow public or link-only distribution without exposing the owner's credentials. #### `POST /v1/skills/@{ns}/{name}/versions/{version}/share` — Auth + email verified + scan report required Create a share link. ```json { "visibility": "private", "expires_in": "never" } ``` - `visibility`: `private` (link-only) or `public` (discoverable via search) - `expires_in`: `1d` | `7d` | `30d` | `never` **Response:** ```json { "ok": true, "data": { "share_id": "shr_xyz789", "url": "https://skillsafe.ai/share/shr_xyz789", "expires_at": null } } ``` #### `GET /v1/share/{shareId}/readme` — No auth The shared version's `SKILL.md` as `text/markdown`. Possession of the link is the authorization, same as `GET /v1/share/{shareId}`. Unlike `/download` this records **no install** — reading what a skill does is not installing it. #### Sharing a skill with someone who has an agent The zero-install way to hand a skill to another person: send them `https://skillsafe.ai/share/{shareId}?md`. That URL returns one self-contained plain-text document — provenance, tree hash, install commands, and the complete `SKILL.md` body — so their agent can fetch it once and use the skill immediately, with nothing installed and no account. Revoked and expired links return 410 with the reason. #### `GET /v1/skills/@{ns}/{name}/versions/{version}/shares` — Auth required List share links for a version. #### `GET /v1/share/{shareId}` — No auth Get share link metadata (namespace, name, version, expiry, scan grade). #### `GET /v1/share/{shareId}/download` — No auth Download the skill archive via share link. #### `DELETE /v1/share/{shareId}` — Auth required Revoke a share link. Revoking all public links reverts skill visibility to private. --- ### Verification #### `POST /v1/skills/@{ns}/{name}/versions/{version}/verify` — Auth required Submit a consumer verification report after installing a skill. Rate limited: 10 req/min. ```json { "scan_report": { "grade": "A", "findings": [] } } ``` **Response:** ```json { "ok": true, "data": { "verdict": "verified", "details": "Consumer scan matches sharer scan. Tree hash verified." } } ``` **Verdicts:** - `verified` — both scans agree, tree hash matches - `divergent` — scans disagree on findings - `critical` — tree hash mismatch — possible tampering — do not use --- ### Authentication All auth endpoints are rate-limited to 10 req/min. #### `POST /v1/auth/google` — No auth Sign in with a Google ID token from Google Sign-In (GSI). Sets `ss_session` HttpOnly cookie. ```json { "credential": "" } ``` #### `POST /v1/auth/github` — No auth Sign in with GitHub OAuth code. ```json { "code": "", "redirect_uri": "https://skillsafe.ai/auth/github/callback" } ``` #### `POST /v1/auth/email/send-code` — No auth Send 6-digit magic code to email. ```json { "email": "user@example.com" } ``` #### `POST /v1/auth/email/verify-code` — No auth Verify code and create session. ```json { "email": "user@example.com", "code": "123456" } ``` #### `POST /v1/auth/email/login` — No auth Sign in with email + password. ```json { "email": "user@example.com", "password": "..." } ``` #### `POST /v1/auth/cli` — No auth Create a CLI device-flow session. The caller opens `login_url` in the browser, then polls. Body (optional): `{ "label": "my-agent" }` — names the minted key. ```json // Response: { "ok": true, "data": { "session_id": "...", "login_url": "https://skillsafe.ai/auth/cli/?session=...", "expires_in": 900 } } ``` #### `GET /v1/auth/cli/{session_id}` — No auth Poll for CLI session approval. ```json // Response when pending: { "ok": true, "data": { "status": "pending" } } // Response when approved (one-time read — the session is deleted): { "ok": true, "data": { "status": "approved", "api_key": "sk_...", "account_id": "acc_...", "username": "alice", "namespace": "@alice" } } ``` #### `POST /v1/auth/cli/{session_id}/approve` — Auth required Approve the CLI session (called by the browser page after user signs in). #### `POST /v1/auth/logout` — Auth required Revoke current session and clear cookie. #### `POST /v1/auth/key-exchange` — Auth (Bearer) required Exchange a Bearer token for an HttpOnly session cookie (browser use). --- ### Account #### `GET /v1/account` — Auth required Also the credential's self-description: `capabilities` reports what THIS key may do, the conditions attached, and how to gain more. Read it instead of hard-coding a capability table or discovering limits through 403s. ```json { "ok": true, "data": { "account_id": "acc_...", "namespace": "alice", "email": "alice@example.com", "email_verified": true, "tier": "free", "avatar_url": "https://...", "public_skill_count": 3, "created_at": "2025-01-01T00:00:00Z", "provisional_expires_at": null, "capabilities": { "credential": "verified", "can": ["skills:save", "skills:share", "apps:create", "apps:schedule", "..."], "cannot": [], "expires_at": null, "days_remaining": null, "upgrade": null, "conditions": {} } } } ``` On a temp key (`POST /v1/auth/temp-key`), `credential` is `"temp"`, `expires_at`/`days_remaining` count down the 7-day TTL, `cannot` lists what needs a verified email, and `conditions` spells out the caveats a bare capability name would misrepresent — e.g. `skills:share` is permitted only for a scan with no **actionable** findings (critical, high, or medium — low and informational do not block), and `apps:schedule` only on a non-public app. See [auth.md](https://skillsafe.ai/auth.md) for the full matrix. Every capability refusal returns `403` with a machine-readable `error.details.reason` (branch on that, never the message) and `error.details.upgrade.steps` naming the endpoints that lift it. #### `POST /v1/account/email/send-code` — Auth required Start attaching an email to the current account — the first half of upgrading a temp key. Sends a 6-digit code. An agent can call this itself; only reading the code needs a human. ```json { "email": "you@example.com" } ``` #### `POST /v1/account/email/verify-code` — Auth required Submit the code to attach and verify the email. This **upgrades the current key in place**: the same key string keeps working with full authority, so nothing needs re-plumbing. Allow ~30s for the auth cache to turn over. If the email already belongs to another account, the provisional account's skills and apps transfer to it instead. ```json { "email": "you@example.com", "code": "123456" } ``` #### `POST /v1/account/verify-email` — Auth required Send email verification link. ```json { "email": "alice@example.com" } ``` #### `GET /v1/account/usage` — Auth required Storage usage, skill count, API key count, tier limits. #### `GET /v1/account/keys` — Auth required List active API keys (max 20 per account). #### `POST /v1/account/keys` — Auth required Create a new API key. Expires after 7 days by default; `expires_in_days` accepts 1–3650 or `null` (never expires). ```json { "label": "CI deployment key", "expires_in_days": 30 } ``` Response includes `api_key` — shown once, store securely. #### `DELETE /v1/account/keys/{keyId}` — Auth required Revoke an API key. #### `DELETE /v1/account/data` — Auth required Purge all skills and data, keep account. #### `DELETE /v1/account` — Auth + email verified required Schedule account deletion (irreversible after grace period). --- ### Organizations Organizations share a namespace and can collaborate on skills. #### `GET /v1/orgs` — Auth required List organizations the current user belongs to. #### `POST /v1/orgs` — Auth + email verified required ```json { "namespace": "acme-ai", "display_name": "Acme AI", "description": "..." } ``` #### `GET /v1/orgs/{orgId}` — Auth required Full details for members; public info for others. #### `PATCH /v1/orgs/{orgId}` — Auth (owner) required Update display_name, description. #### `DELETE /v1/orgs/{orgId}` — Auth (owner) required Delete org and all its skills. #### `GET /v1/orgs/{orgId}/members` — Auth required #### `POST /v1/orgs/{orgId}/members/invite` — Auth (owner/admin) required ```json { "email": "bob@example.com", "role": "member" } ``` #### `PATCH /v1/orgs/{orgId}/members/{accountId}` — Auth (owner) required Change member role. Roles: `owner`, `admin`, `member`. #### `DELETE /v1/orgs/{orgId}/members/{accountId}` — Auth (owner/admin) required Remove member. #### `POST /v1/orgs/{orgId}/domain` — Auth (owner) required Set domain for member verification. ```json { "domain": "acme.com" } ``` #### `POST /v1/orgs/{orgId}/domain/verify` — Auth (owner) required Verify domain via DNS TXT record. #### `POST /v1/orgs/{orgId}/join-link` — Auth (owner/admin) required Create or regenerate a join link for direct membership. #### `GET /v1/orgs/join/{linkId}` — No auth Get public org info for a join link. #### `POST /v1/orgs/join/{linkId}/accept` — Auth required Join org via link. #### `POST /v1/fleet/report` — Auth required Report one machine's installed skills to the org fleet inventory. This is what `skillsafe report` sends; call it directly to report from CI or a managed host. Body carries the machine label and the installed set (name, version, tree hash) so drift and unverified installs across a fleet are visible centrally. --- ### Billing All billing endpoints require auth and are rate-limited to 30 req/min. #### `GET /v1/billing/config` Stripe publishable key. #### `POST /v1/billing/checkout` — Email verified required Create Stripe checkout session for plan upgrade. ```json { "price_id": "price_xxx", "success_url": "https://skillsafe.ai/dashboard", "cancel_url": "https://skillsafe.ai/pricing" } ``` #### `GET /v1/billing/portal` Stripe billing portal session URL. #### `GET /v1/billing/subscription` Current plan, status, renewal date. #### `GET /v1/billing/invoices` Invoice history. Params: `limit`, `cursor`. #### `GET /v1/billing/payment-methods` Saved payment methods. #### `POST /v1/billing/cancel` Cancel subscription (active until period end). #### `POST /v1/billing/reactivate` Reactivate a cancelled subscription. #### `POST /v1/billing/seats` Change seats on a Team/Enterprise subscription. Body: `{ "quantity" }` (Team 1+, Enterprise 5+, max 1000; never below the org's current member count). Prorated onto the next invoice. --- ### Apps & Deploy Hosted mini-apps at `https://{slug}.skillsafe.ai`, each running a scanned, sandboxed agent. Agents drive the platform API directly — served on deploy.skillsafe.ai itself (`/v1/*`, Bearer auth only) as well as api.skillsafe.ai; full agent-facing guide: https://deploy.skillsafe.ai/llms.txt. Humans can instead use the App Creator chat builder at https://creator.skillsafe.ai/ (browser sign-in). #### `GET /v1/creator/me` — Auth (API key or creator session) required Agent bootstrap: the caller's username, daily chat usage, + owned apps: `{ "username", "daily": {"used", "cap"}, "apps": [...] }`. #### `POST /v1/creator/chat` — Auth (creator browser session only) required One App Creator agent turn over SSE (Workers AI gemma). Browser feature: requires a creator-scoped `aut_*` session minted by signing in at creator.skillsafe.ai — API keys get 401 (agents: use the platform API directly). Body: `{ "messages": [{"role", "content"}], "confirmations"?, "attachments"? }` (client-held history; last message must be `user`). Events: `text`, `tool_start`, `tool_result`, `confirm_request`, `error`, `done`, `ping`. Free, daily-capped (50 msgs/day), one turn in flight per account (409 while busy). #### `POST /v1/apps` — Auth + email verified required Create a hosted app. Body: `{ "slug", "title", "system_prompt" | "skill_id", "description"?, "model"?, "markup_bps"?, "guest_enabled"? }` — exactly one of `system_prompt` (raw agent prompt, security-scanned) or `skill_id` (owned skill whose SKILL.md is snapshotted). Runs are billed usage-based: users pay the actual compute cost of each run plus a 10% platform margin (1 credit = $0.0001 — $1 = 10,000 credits; min 1 credit), plus the owner's markup (`markup_bps`, 0–10000 = 0–100%) applied to the same base — the markup is the owner's earnings and is credited in full, with no platform fee deducted. #### `GET /v1/apps` — Auth required List the caller's apps. #### `GET /v1/apps/{slug}` / `PATCH` / `DELETE` — Auth (owner) required Publisher view / update metadata + `visibility` (`PATCH {"visibility": "public"}` publishes; requires clean scans) / soft-delete. #### `POST /v1/apps/{slug}/releases` — Auth (owner) required Upload a frontend: `{ "files": [{"path", "content"}] }` (static bundle) or `{ "spec": {...} }` (declarative App Spec, platform-rendered). The whole bundle is security-scanned; critical/high findings block the release. #### App runtime data layer — `/v1/app-api/collections*` Apps get declared **collections** of typed records with per-collection read/write ACLs, a constrained query DSL (`POST .../{c}/query` — filter/sort/keyset-paginate), and quotas, declared in the release manifest (`spec.collections`, or a top-level `collections` field on a file release). A collection may also declare `embed: ["field", …]` (max 4 string fields), which embeds those fields on write and enables **vector similarity search**: `POST /v1/app-api/collections/{c}/similar` with `{"text", "limit"}` returns records ordered by semantic similarity, ACL-filtered like every read (`text` max 2,000 chars, `limit` max 20, 30 req/min). Indexing is asynchronous and there is no backfill — only records written after `embed` was declared are searchable. Full reference: https://deploy.skillsafe.ai/llms.txt #### `GET /v1/apps/search` — No auth Public app directory (public + active apps). #### `GET /v1/apps/{slug}/public` — No auth Public listing for one discoverable app. --- ## Security Model **Save-first:** Skills are saved privately (no scan required). Sharing is opt-in and requires a scan report. **Dual-side verification:** 1. Publisher saves a skill — server runs security scan automatically 2. Server stores the scan report alongside the version 3. Consumer downloads and installs — server re-scans independently 4. Server compares reports + tree hash → returns verdict **Tree hash:** SHA-256 of sorted file manifest. Detects any tampering between save and install. **Scanner passes:** 1. Python AST — `eval()`, `exec()`, `os.system()`, `subprocess.*` 2. JS/TS regex — `eval()`, `new Function()`, `child_process` 3. Secret detection — AWS keys, GitHub tokens, private keys 4. Prompt injection — manipulation patterns in `.md` files 5. Shell/threat patterns — exfiltration, reverse shells, reconnaissance 6. Binary file detection — `.exe`, `.dll`, `.so`, `.dylib` 7. Base64 deep-scan — decode and re-check payloads 8. Unicode obfuscation — zero-width chars, homograph spoofing **Grades:** A (clean) → F (critical findings) **Verdicts:** `verified` (safe) | `divergent` (review carefully) | `critical` (possible tampering, do not install)