---
summary: "Every ScaiSkills endpoint \u2014 skills, versions, bindings, grants, resolve,\
  \ and the progressive-disclosure MCP surface."
title: API reference
path: reference/api
status: published
---

# API reference

All endpoints are mounted at `/v1/modules/scaiskills/` and authenticate with the standard ScaiGrid bearer token. Responses use ScaiGrid's standard envelope (`{ "data": ... }` for success, `{ "error": ... }` for failures). Permission checks happen per-endpoint against the keys documented in [Permissions](./permissions).

## Skills

### `POST /skills`

Register a new skill identity. Requires `scaiskills:publish`.

| Field | Required | Notes |
|---|---|---|
| `slug` | yes | Lowercase letters, digits, hyphens. Pattern: `^[a-z][a-z0-9-]{2,63}$`. Globally unique. |
| `visibility` | no | `private` (default) or `public`. |
| `description` | no | Free text, up to 500 chars. |

```bash
curl -X POST "$SCAIGRID_HOST/v1/modules/scaiskills/skills" \
  -H "Authorization: Bearer $SCAIGRID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"slug": "summarise", "visibility": "private"}'
```

Returns `{ "data": { "id", "slug", "owner_workspace_id", "visibility", "description", "created_at" } }`.

### `GET /skills`

List skills visible to the caller's workspace — own skills plus all public skills across the deployment. Requires `scaiskills:view`.

### `GET /skills/{slug}`

Fetch one skill's metadata and full version list. Requires `scaiskills:view`. Private skills owned by another workspace return `SCAISKILLS_SKILL_NOT_FOUND`, not `403`.

```json
{
  "data": {
    "id": "skill_abc",
    "slug": "summarise",
    "owner_workspace_id": "ten_xyz",
    "visibility": "private",
    "description": "...",
    "created_at": "2026-05-17T10:00:00Z",
    "versions": [
      {
        "id": "ver_001",
        "semver": "0.1.0",
        "status": "published",
        "content_hash": "sha256:...",
        "published_at": "..."
      }
    ]
  }
}
```

### `DELETE /skills/{slug}`

Hard-delete the skill, every version, every binding, and every grant. Requires `scaiskills:manage`. Workspace must own the skill. Returns `{ "deleted": true, "slug": "..." }`.

## Versions

### `POST /skills/{slug}/versions`

Publish a new version. Requires `scaiskills:publish` and ownership of the skill. Multipart upload with one field, `bundle`, containing the `.tar.gz`.

```bash
curl -X POST "$SCAIGRID_HOST/v1/modules/scaiskills/skills/summarise/versions" \
  -H "Authorization: Bearer $SCAIGRID_API_KEY" \
  -F "bundle=@summarise-0.1.0.tar.gz"
```

The validator checks:

- Bundle is a valid gzip-compressed tar.
- `SKILL.md` is present at the root with valid YAML frontmatter.
- Manifest `name` equals the URL slug.
- Manifest `version` is strictly greater than every previously published semver for this skill.
- References paths don't escape the bundle root.
- Secrets entries match the v1.1 schema.

On success returns the new version row including `content_hash`, `storage_uri`, and `status: "published"`.

On validation failure returns a non-standard error envelope with `success: false` and a `context.errors` array:

```json
{
  "success": false,
  "error": {
    "code": "SCAISKILLS_VALIDATION_FAILED",
    "message": "Bundle validation failed",
    "context": {
      "errors": [
        {"code": "MANIFEST_VERSION_NOT_MONOTONIC", "message": "...", "location": "SKILL.md:3"}
      ]
    }
  }
}
```

### `POST /skills/{slug}/versions/{semver}/yank`

Mark a version as yanked. Requires `scaiskills:publish` and ownership. Existing bindings pinned to the version keep working; new bindings against the exact semver fail with `SCAISKILLS_YANKED_VERSION`; floating refs (`latest`, `^x.y`) skip yanked versions.

Returns `{ "semver": "0.2.0", "status": "yanked" }`.

## Bindings

### `POST /bindings`

Create a binding with eager dependency resolution. Requires `scaiskills:bind`.

| Field | Required | Notes |
|---|---|---|
| `skill_id` | yes | The skill's id (not slug). |
| `version` | yes | `0.1.0` (exact), `latest`, `^1.2`, `~1.2`, `>=1.0`. The leading `@` is optional. |
| `scope_type` | yes | `workspace`, `channel`, `user`, or `core`. |
| `scope_id` | yes | Id under the scope type. |
| `secret_mappings` | no | `{ "secret_name": "vault_path" }`. Required entries for any `required: true` secret in the manifest. |

What happens server-side:

1. Resolve the version ref against published, non-yanked versions.
2. Walk `manifest.requires.skills` depth-first, resolve each dependency, detect cycles, build the lockfile.
3. Check granted permissions vs. declared permissions, mapped secrets vs. required secrets, set `pending_grants`.
4. Persist the binding row with `resolved_deps_json` and `secret_mappings_json`.
5. Invalidate the Redis cache for the scope.

Returns the full binding row.

### `GET /bindings?scope_type=&scope_id=`

List bindings for a given scope. Requires `scaiskills:view`. Both query parameters are required.

### `DELETE /bindings/{binding_id}`

Delete a binding and cascade its grants. Requires `scaiskills:bind`. Invalidates the scope's Redis cache. Returns `{ "deleted": true }` (or `{ "deleted": false }` if the id wasn't found — idempotent).

### `POST /bindings/{binding_id}/permissions/grant`

Grant one declared permission on a binding. Requires `scaiskills:grant`.

```json
{
  "permission": "scaidrive:read:/policies/"
}
```

After the grant, the binding's manifest is rechecked; if every declared permission is granted and every required secret is mapped, `pending_grants` flips to `false`. Scope cache is invalidated.

Returns `{ "id", "binding_id", "permission_string", "granted_at" }`.

## Resolve

### `POST /resolve`

Return the lightweight manifest list for a scope. Requires `scaiskills:view`. Called by the runtime — ScaiWave per turn (`scope_type: channel | user`), ScaiCore at boot (`scope_type: core`).

```json
{
  "scope_type": "channel",
  "workspace_id": "ten_xyz",
  "channel_id": "chn_support",
  "user_id": "usr_alice"
}
```

The ancillary `workspace_id`, `channel_id`, `user_id`, and `core_id` fields are merged into the candidate set; `scope_type` is the primary scope and dictates the Redis cache key.

Returns:

```json
{
  "data": {
    "skills": [
      {
        "slug": "summarise",
        "version": "1.0.0",
        "description": "...",
        "triggers": ["summarise", "tl;dr"]
      }
    ],
    "cache_ttl_ms": 60000
  }
}
```

Active, non-`pending_grants` bindings only. Deduplicated by skill_id with precedence `core > user > channel > workspace`. Cached in Redis for 60 seconds; every binding write for the scope invalidates.

## MCP tools

The progressive-disclosure surface the LLM calls during a turn. Each call logs a row in `mod_scaiskills_invocations` for metering.

### `POST /mcp/skills/list`

The lightweight manifest list for the caller's scope. Identical body shape to `/resolve`; identical output shape. Requires `scaiskills:view`.

### `POST /mcp/skills/search`

Semantic search over bound skills. Query parameter `query` plus the same scope body as `/mcp/skills/list`. Requires `scaiskills:view`.

```bash
curl -X POST "$SCAIGRID_HOST/v1/modules/scaiskills/mcp/skills/search?query=refund" \
  -H "Authorization: Bearer $SCAIGRID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"scope_type": "workspace", "workspace_id": "ten_xyz"}'
```

Two-stage pipeline: a ScaiMatrix vector search over the indexed manifests, intersected against the caller's resolved set so out-of-scope skills cannot leak. On ScaiMatrix failure, falls back to a substring filter over the resolved set. Each match includes `score` and `match_excerpt`.

### `POST /mcp/skills/view/{slug}`

Return the full `SKILL.md` body (default) or a file under `references/`. Requires `scaiskills:view` and an active binding for the slug in the caller's scope — otherwise `SCAISKILLS_SKILL_NOT_FOUND`.

Query parameter `path` selects an alternative file: `references/refund-policy.md` (or just `refund-policy.md` — the `references/` prefix is added if missing). Absolute paths and `..` segments are rejected.

Returns:

```json
{
  "data": {
    "content": "When asked for a summary, follow these patterns...",
    "content_type": "text/markdown"
  }
}
```

The YAML frontmatter is stripped from `SKILL.md` so the LLM only sees the body.

## Errors

ScaiSkills-specific codes:

| Code | HTTP | Meaning |
|---|---|---|
| `SCAISKILLS_SKILL_NOT_FOUND` | 404 | Slug doesn't exist or isn't visible to the caller. |
| `SCAISKILLS_VERSION_NOT_FOUND` | 404 | No version with this semver for the skill. |
| `SCAISKILLS_BINDING_NOT_FOUND` | 404 | Binding id doesn't exist. |
| `SCAISKILLS_SLUG_CONFLICT` | 409 | Slug already taken (globally). |
| `SCAISKILLS_VERSION_CONFLICT` | 409 | Version exists or is not strictly greater than the previous version. |
| `SCAISKILLS_VALIDATION_FAILED` | 422 | Bundle or manifest didn't validate. `context.errors` enumerates. |
| `SCAISKILLS_DEPENDENCY_CYCLE` | 422 | `requires.skills` formed a cycle during bind resolution. |
| `SCAISKILLS_UNRESOLVABLE_DEPENDENCY` | 422 | A dependency ref didn't match any published version. |
| `SCAISKILLS_PENDING_GRANTS` | 409 | Binding has ungranted permissions or unmapped required secrets when an active path was attempted. |
| `SCAISKILLS_YANKED_VERSION` | 410 | Tried to bind to a yanked version. |
| `SCAISKILLS_BUNDLE_TOO_LARGE` | 413 | Bundle exceeded the size cap. |
| `SCAISKILLS_STORAGE_ERROR` | 500 | S3 put/get failed. |

All errors follow ScaiGrid's standard envelope with `code`, `message`, optional `details`, and a request id in `meta`.
