REST API
Everything the skillsvault CLI does, it does over this API — and so can you. Use it to publish skills from CI, mirror your registry, fetch and verify the signed policy bundle, or stream audit events into your own systems. All endpoints are JSON in / JSON out and authenticate with an agent key.
Base URL
The base URL is your organisation's skillsvault control-plane origin (for example https://skillsvault.io). Every path below is relative to it. The :orgSlug path segment is your organisation's slug.
Authentication
Send your agent key on every request (except GET /api/bundle/key, which is public). Either header works:
X-Skillsvault-Key: skillsvault_ak_…
Authorization: Bearer skillsvault_ak_…
A key is scoped to exactly one organisation. Create or revoke keys in Settings → API keys; see Install & connect for the full lifecycle.
401— the key is missing or unknown.403— the key is valid but used against a different org than the one in the path.
Conventions
Requests and responses are JSON unless noted. Send Content-Type: application/json on writes. A minimal authenticated call looks like:
curl https://skillsvault.io/api/skills/acme \
-H "X-Skillsvault-Key: skillsvault_ak_live_xxx"
Error responses carry a JSON body describing the problem; see Errors for the status codes you should handle.
GET /api/skills/:orgSlug
List the organisation's skills and their current state — the same manifest the CLI uses to decide what to install and what to remove. The skillsvault/skillsvault onboarding skill is always included.
Auth: agent key.
Query
| Param | Required | Description |
|---|---|---|
kinds | no | Comma-separated artifact kinds the client can install: skill, subagent, command. Default skill — a client that doesn't ask is never sent a subagent or command. The CLI sends all three. |
Response
{
"org": "acme",
"kinds": ["skill"],
"skills": [
{
"id": "6f1c…",
"name": "acme/hubspot-crm-entry",
"kind": "skill",
"version": "1.3.0",
"status": "active",
"compliance": "compliant",
"content_hash": "sha256:77de…",
"url": "https://skillsvault.io/acme/skills/6f1c…"
}
]
}
status is one of active, deprecated, or banned; compliance is compliant or non_compliant. Banned skills are listed so a client can remove them.
curl https://skillsvault.io/api/skills/acme \
-H "X-Skillsvault-Key: skillsvault_ak_live_xxx"
Status: 200 · 401 · 403.
GET /api/skills/:orgSlug/content
Fetch the full content of one skill's current version — its SKILL.md and every bundled file.
Auth: agent key.
Query
| Param | Required | Description |
|---|---|---|
skill | yes | The namespaced skill name, e.g. acme/hubspot-crm-entry. |
Response
{
"name": "acme/hubspot-crm-entry",
"version": "1.3.0",
"status": "active",
"skill_md": "---\nname: hubspot-crm-entry\n…",
"files": {
"scripts/run.py": "…",
"references/fields.md": "…"
}
}
files maps each relative path to its text content.
curl "https://skillsvault.io/api/skills/acme/content?skill=acme/hubspot-crm-entry" \
-H "X-Skillsvault-Key: skillsvault_ak_live_xxx"
Status: 200 · 400 (missing ?skill) · 401 · 403 · 404 (skill not found).
POST /api/skills/:orgSlug/publish
Publish a skill folder as a new immutable version — the API behind skillsvault publish. See Authoring & publishing and the manifest reference.
Auth: agent key.
Body
| Field | Required | Description |
|---|---|---|
name | yes | Namespaced namespace/skill, lowercase. |
version | yes | Semantic version, e.g. 1.4.0. |
skill_md | yes | The full SKILL.md text. |
kind | no | skill (default), subagent, or command. |
description | no | Discovery description. Used only when the SKILL.md frontmatter has no description; the frontmatter's wins. |
intent | no | Machine intent (kebab-case). |
targets | no | Array of systems/processes the skill touches. |
files | no | Map of relative path → file content for bundled scripts/, references/, assets/. |
manifest | no | The parsed skillsvault.json object. |
{
"name": "acme/hubspot-crm-entry",
"version": "1.4.0",
"description": "Create and update HubSpot contacts from a deal brief.",
"intent": "crm-data-entry",
"targets": ["hubspot"],
"skill_md": "---\nname: hubspot-crm-entry\n…",
"files": { "scripts/run.py": "…" },
"manifest": { "skillsvault_schema": 1, "name": "acme/hubspot-crm-entry", "…": "…" }
}
Response
{
"ok": true,
"skill": "acme/hubspot-crm-entry",
"kind": "skill",
"version": "1.4.0",
"content_hash": "sha256:9a1b…"
}
Re-publishing an existing version with identical content succeeds with "unchanged": true. A new namespace is created on first publish, within the plan's namespace limit.
A member's key: 202, submitted for review. When the key belongs to someone with the member role, nothing is published. The skill is recorded as a submission for review, and the response is 202 with submitted: true, the submission_id and the url of the submission in the console. Only skills can be submitted this way; a subagent or command from a member's key is refused with 403.
{
"ok": true,
"submitted": true,
"submission_id": "6f1c…",
"url": "https://skillsvault.io/acme/requests/6f1c…",
"skill": "acme/hubspot-crm-entry",
"error": "Not published yet: submitted for review. An owner, admin or governance officer will approve it at https://skillsvault.io/acme/requests/6f1c…"
}
The error field is there for CLIs before 0.6.5, which print it for any status other than 200.
curl -X POST https://skillsvault.io/api/skills/acme/publish \
-H "X-Skillsvault-Key: skillsvault_ak_live_xxx" \
-H "Content-Type: application/json" \
-d @publish.json
Status: 200 · 202 (a member's key: submitted for review, not published) · 400 (bad JSON) · 401 · 402 (a new namespace would exceed the plan's limit) · 403 (a key for another org, or a role that can't publish) · 409 (this version is already published with different content) · 413 (content too large) · 422 (missing required field, invalid name or version, unknown kind) · 429 (rate limited).
GET /api/bundle/:orgSlug
Fetch the signed policy bundle — the snapshot of approved skills and active policies that the gate evaluates locally.
Auth: agent key.
Response — a signed envelope:
{
"alg": "Ed25519",
"payload": "{\"bundle_version\":1750848000, …}",
"signature": "base64…"
}
payload is a JSON string: verify signature over its exact bytes using the public key, then parse it. The decoded payload contains:
| Field | Meaning |
|---|---|
bundle_version | When the bundle was compiled, as Unix seconds — compiled fresh on each request. |
defaults | Fallback actions (unmanaged, managed_missing_manifest) and staleness settings. |
namespaces | The managed namespaces. |
manifest_index | Map of skill → current_version, content_hash, status, targets, kind. |
policies | Ordered list of policy_id, name, matcher_type, match_value, action, reason. |
curl https://skillsvault.io/api/bundle/acme \
-H "X-Skillsvault-Key: skillsvault_ak_live_xxx"
Status: 200 · 401 · 403 (unknown org, or a key for another org — the same answer, so a key can't probe which orgs exist).
GET /api/bundle/key
Return the deployment's Ed25519 public bundle-signing key, PEM-encoded, as text/plain. It is one key for the whole skillsvault deployment — the same for every organisation — not a per-organisation key. Use it to verify the signature on a bundle from GET /api/bundle/:orgSlug. This endpoint is public — no agent key required.
curl https://skillsvault.io/api/bundle/key
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEA…
-----END PUBLIC KEY-----
Status: 200.
POST /api/audit/ingest
Append one or more audit events — the API the gate uses to record every decision. See Audit log.
Auth: agent key.
Body
{
"events": [
{
"skill_name": "acme/teamleader-crm-entry",
"skill_version": "2.0.0",
"harness": "claude_code",
"machine_name": "lab-mbp",
"decision": "blocked",
"policy_id": "pol_7731",
"policy_name": "Teamleader retired",
"reason": "Use acme/hubspot-crm-entry instead.",
"occurred_at": "2026-06-25T10:03:12Z",
"event_id": "0b6f8a52-3c1e-4d7a-9f0e-6a2b51c4d9e3"
}
]
}
decision is one of allowed, warned, or blocked. Up to 500 events per request. skill_version, harness, machine_name, policy_id, policy_name, reason, occurred_at and event_id are optional. The event is stamped with the server's receipt time — a client-supplied occurred_at is accepted but not stored, so a key can't backdate evidence — and a policy_id that isn't one of your org's policies is dropped (the policy_name is kept). event_id (up to 64 characters) makes delivery idempotent: an event whose id your org already holds is skipped, so a client can safely resend a batch whose response it never saw. received counts the events accepted in the request, skipped duplicates included.
Response
{ "received": 1 }
curl -X POST https://skillsvault.io/api/audit/ingest \
-H "X-Skillsvault-Key: skillsvault_ak_live_xxx" \
-H "Content-Type: application/json" \
-d @events.json
Status: 200 · 400 (bad JSON) · 401 · 413 (body too large) · 422 (invalid event).
POST /api/devices/heartbeat
Report that a machine is alive, its installed inventory, its CLI version and what its gate hook covers — and learn whether the CLI is out of date. Sent automatically by the CLI on login, sync, try and pull.
Auth: agent key.
Body
{
"harness": "claude_code",
"machine_name": "lab-mbp",
"plugin_version": "0.6.5",
"installed": [
{ "skill": "acme/hubspot-crm-entry", "version": "1.3.0", "status": "active" }
],
"hooks_enabled": true,
"hooks_coverage": ["skill", "agent", "task", "prompt"],
"hooks_attestation_version": 2,
"hooks_config_hash": "sha256:4f3d…"
}
All fields are optional (harness → claude_code, machine_name → unknown). Leave installed out for a liveness-only report — the stored inventory is kept; an empty array means nothing is installed. hooks_coverage lists which invocation paths a hook running the gate covers: skill (the Skill tool), agent and task (subagents), prompt (typed /commands); all four is Gating: full on the Harnesses page. The hook fields are self-reported by the CLI — they catch a misconfigured machine, not a determined user.
Response
{
"ok": true,
"installed": 1,
"latest_cli": "0.6.5",
"min_cli": "0.5.1",
"outdated": false,
"unsupported": false,
"upgrade_url": "curl -fsSL https://skillsvault.io/install.sh | sh"
}
outdated is true below the latest release (the CLI prints an upgrade hint); unsupported is true below the minimum supported version. installed is null for a liveness-only report.
Status: 200 · 400 (bad JSON) · 401 · 409 (this machine name is registered to another member) · 413 · 422.
POST /api/devices/inventory
Upload a machine's scan — what skillsvault login --import-scan sends. The body is the scan as the CLI builds it (schema_version, cli_version, machine_name, with_content, truncated, items); see Importing a scan for exactly which fields an item carries and when file content is included. Each upload replaces the machine's previous one.
Auth: agent key.
Response
{ "ok": true, "items": 5, "unmanaged": 4, "flagged": 1, "with_content": 0 }
Status: 200 · 400 (bad JSON) · 401 · 409 (this machine is registered to another member) · 413 · 422.
CLI login (device flow)
Connecting a machine with skillsvault login uses a browser device-authorization flow: the CLI calls POST /api/cli/device/start to obtain a device_code and a short user_code, opens the verification page, then polls POST /api/cli/device/poll with the device_code until you approve it in the browser — at which point it receives a real agent key. Integrators normally just run skillsvault login (or skillsvault configure with a key) rather than calling these endpoints directly.
Errors
| Status | Meaning |
|---|---|
400 | Malformed request (bad JSON, or a missing required query parameter). |
401 | Missing or unknown agent key. |
402 | The plan's limit would be exceeded (for example a new namespace on publish). |
403 | Valid key used against a different organisation than the path, or an unknown org. |
404 | Skill not found. |
409 | Conflict — a version already published with different content, or a machine registered to another member. |
413 | Request body or content too large. |
422 | Validation failed — a required field is missing or a name is malformed. |
429 | Rate limited. The body is { "error": "…", "retry_after": <seconds> } and the Retry-After header gives the same number of seconds; wait that long, then retry. |
A 202 from publish is not an error: the key belongs to a member, so the skill was submitted for review instead of published.
Rate limits
The agent-key endpoints are limited per key, in fixed one-minute windows:
| Endpoint | Requests per key per minute |
|---|---|
POST /api/skills/:orgSlug/publish | 60 |
GET /api/bundle/:orgSlug | 120 |
POST /api/audit/ingest | 600 |
POST /api/devices/heartbeat | 60 |
POST /api/devices/inventory | 30 |
MCP server (/api/mcp) | 300 |