Skip to content

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

ParamRequiredDescription
kindsnoComma-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

ParamRequiredDescription
skillyesThe 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

FieldRequiredDescription
nameyesNamespaced namespace/skill, lowercase.
versionyesSemantic version, e.g. 1.4.0.
skill_mdyesThe full SKILL.md text.
kindnoskill (default), subagent, or command.
descriptionnoDiscovery description. Used only when the SKILL.md frontmatter has no description; the frontmatter's wins.
intentnoMachine intent (kebab-case).
targetsnoArray of systems/processes the skill touches.
filesnoMap of relative path → file content for bundled scripts/, references/, assets/.
manifestnoThe 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:

FieldMeaning
bundle_versionWhen the bundle was compiled, as Unix seconds — compiled fresh on each request.
defaultsFallback actions (unmanaged, managed_missing_manifest) and staleness settings.
namespacesThe managed namespaces.
manifest_indexMap of skill → current_version, content_hash, status, targets, kind.
policiesOrdered 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

StatusMeaning
400Malformed request (bad JSON, or a missing required query parameter).
401Missing or unknown agent key.
402The plan's limit would be exceeded (for example a new namespace on publish).
403Valid key used against a different organisation than the path, or an unknown org.
404Skill not found.
409Conflict — a version already published with different content, or a machine registered to another member.
413Request body or content too large.
422Validation failed — a required field is missing or a name is malformed.
429Rate 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:

EndpointRequests per key per minute
POST /api/skills/:orgSlug/publish60
GET /api/bundle/:orgSlug120
POST /api/audit/ingest600
POST /api/devices/heartbeat60
POST /api/devices/inventory30
MCP server (/api/mcp)300