Skip to content

Reading skills

Four endpoints, in the order you would normally call them: find out what your key can reach, resolve a project's skill set, fetch one skill, then keep a local copy in step.

GET /skills/whoamiwho this key is, and which projects it reaches
GET /skills/resolvethe effective skill set for a project
GET /skills/by-name/{name}one skill, including its body
GET /skills/diffcompare what you hold against the server

All four are available on SaaS and on-prem.

Transport

Every endpoint on this page accepts the key either way — as an X-API-Key header or as an ?api_key= query parameter:

bash
curl -s "$IM_URL/skills/resolve?project_id=$PID" -H "X-API-Key: $IM_KEY"
curl -s "$IM_URL/skills/resolve?project_id=$PID&api_key=$IM_KEY"

Publishing does not accept the query parameter

POST /skills/active-version reads the header only. Carrying the ?api_key= habit over from this page returns 401 API key required, which reads like a bad key rather than a misplaced one. See Publishing.

Authorization

These endpoints require the key owner to be a member of the project — being in its organisation is not enough. Super Admins count as members of every project in their organisation, resolved per request.

GET /skills/bundle is the exception and authorises on the skill instead. Everything else here follows the rule above.

whoami

GET /skills/whoami

Takes no parameters beyond the key. Start here — it answers "which project_id?".

bash
curl -s "$IM_URL/skills/whoami" -H "X-API-Key: $IM_KEY"
json
{
  "user_id": "cd5c29a0-7b51-4f1f-8381-1b0c213e7131",
  "email": "you@example.com",
  "organizations": [
    { "id": "d7fb6107-1188-4f7c-97e2-978403f3e3be", "name": "Optisol" },
    { "id": "d318cf5e-51f3-451e-9c98-3c9bfbce6126", "name": "Demo Organization" }
  ],
  "accessible_projects": 3,
  "projects": [
    { "id": "3a83bbe8-6a99-4137-a3d9-7e4dc72e5bfe", "name": "sample project" },
    { "id": "8112d9c4-8334-43dd-a3c0-436b5cf1f475", "name": "sample" },
    { "id": "873e1fe9-c13e-4a51-8727-ba039096e7f5", "name": "sample project" }
  ],
  "key_id": "b2081d28-27d3-4ec3-b70b-a7083e5cf1d5",
  "key_created_at": "2026-08-26T12:20:13.052199"
}
emailstring

The key owner's address, not the key's. A key carries a person's identity and its reach follows their membership — so this is how you tell whose access you are actually using. See Authentication.

projectsarray

Every project the owner can reach, with the id you need for the other endpoints. Project names are not unique — two entries above share the name sample project — so key your integration on the id.

resolve

GET /skills/resolve

The effective skill set for a project after tier resolution and shadowing — what a consumer in that project would actually get, rather than a union you have to resolve yourself. This is what pm-skills pull uses. See Resolution & pins.

bash
curl -s "$IM_URL/skills/resolve?project_id=$PID&format=index" -H "X-API-Key: $IM_KEY"
ParameterRequiredNotes
project_idyes
formatnofull (the default) or index
environmentnoechoed back, nothing more — see below

format defaults to full

Omitting it returns every skill body in the response. Pass format=index when you only need to know what is there.

json
{
  "project_id": "8112d9c4-8334-43dd-a3c0-436b5cf1f475",
  "environment": null,
  "skills": [
    {
      "name": "sonar-clean-code",
      "version": {
        "sha": "aacea0c4cd13",
        "skill_id": "5adfded2-758e-48c7-9ed9-25b7a3c3a4f5",
        "frontmatter": { "name": "sonar-clean-code", "description": "Enforce SonarQube clean-code rules…" },
        "body": "",
        "message": "Promoted from project sonar-clean-code",
        "status": "active",
        "version_label": "v1.0",
        "resources": [],
        "scan": null,
        "published_via": null
      },
      "resolution": { "tier": "organization", "shadowed": [], "pinned": false }
    },
    {
      "name": "test",
      "version": { "sha": "a28b804cda57", "version_label": "v1.0", "published_via": "direct", "…": "…" },
      "resolution": { "tier": "project", "shadowed": [], "pinned": false }
    }
  ]
}
resolution.tierstring

Which tier won for this project — project, organization or global. Above, sonar-clean-code resolved from the organisation and test from the project itself.

resolution.shadowedarray

What this skill hid. A project-tier skill shadows an organisation-tier one of the same name, and each entry names the tier that lost. Empty when nothing was hidden.

resolution.pinnedboolean

true when the project holds this skill at a fixed sha rather than following its active version.

environmentstring | null

Echoed straight back. Skills are not environment-gated — the parameter exists so a lockfile can record which environment it was generated for.

This is a real difference from prompts, where environment filters and can produce a 409. The parameter looks the same on both and behaves differently.

What format=index actually trims

index empties body and resources. Everything else — frontmatter (including the full description), message, status, version_label, scan — is still there. It is a lighter response, not a summary one.

by-name

GET /skills/by-name/{name}

One resolved skill, including its body.

bash
curl -s -X GET \
  "$IM_URL/skills/by-name/BusinessAnalystAgent?project_id=$PID&api_key=$IM_KEY" \
  -H "Accept: application/json"
ParameterRequiredNotes
nameyespath segment; exact and case-sensitive
project_idyesresolution happens in this project's context
versionnoa sha, or latest (the default)

The response is the version object on its own — the same shape resolve nests under version:

json
{
  "sha": "a28b804cda57",
  "skill_id": "a0132e30-e7c4-4b78-8719-d8e20cbe44bb",
  "frontmatter": {
    "name": "test",
    "description": "aad",
    "license": null,
    "compatibility": null,
    "allowed_tools": null,
    "metadata": {}
  },
  "body": "adfasdf",
  "message": "hello world",
  "status": "active",
  "major_version": 1,
  "minor_version": 0,
  "version_label": "v1.0",
  "resources": [],
  "created_by_name": "Mohanpathi S",
  "created_at": "2026-08-25T12:22:31.096911",
  "scan": {
    "engine": "skillspector",
    "engine_version": "2.2.3",
    "verdict": "clean",
    "risk_score": 0,
    "findings": []
  },
  "published_via": "direct"
}

No name, and no tier, in the response

There is no wrapper — so the reply does not tell you which tier the skill resolved from. If you need that, use resolve, which returns a resolution block per skill.

shastring

Identifies this exact version. Keep it — publishing over this skill requires it as expected_active_sha. See Publishing.

scanobject | null

The security scan report, or null if this version was never scanned. verdict is the field to read.

published_viastring | null

How this version reached production: api, direct, or null for the review workflow. See Publishing flows.

Asking for a specific version

version accepts a sha, but only one belonging to the skill that name resolves to. A sha from a different skill is a 404, not someone else's content:

bash
# a28b804cda57 belongs to `test` → 200
curl -s "$IM_URL/skills/by-name/test?project_id=$PID&version=a28b804cda57" -H "X-API-Key: $IM_KEY"

# aacea0c4cd13 belongs to `sonar-clean-code` → 404
curl -s "$IM_URL/skills/by-name/test?project_id=$PID&version=aacea0c4cd13" -H "X-API-Key: $IM_KEY"

diff

GET /skills/diff

Compare what you hold locally against current resolution — how pm-skills status decides what is stale.

bash
curl -s -G "$IM_URL/skills/diff" \
  --data-urlencode "project_id=$PID" \
  --data-urlencode 'local={"test":"oldsha123456"}' \
  -H "X-API-Key: $IM_KEY"
ParameterRequiredNotes
project_idyes
localnoJSON object of {name: sha}; defaults to {}
json
{
  "project_id": "8112d9c4-8334-43dd-a3c0-436b5cf1f475",
  "entries": [
    {
      "name": "sonar-clean-code",
      "state": "added",
      "local_sha": null,
      "server_sha": "aacea0c4cd13",
      "tier": "organization"
    },
    {
      "name": "test",
      "state": "updated",
      "local_sha": "oldsha123456",
      "server_sha": "a28b804cda57",
      "tier": "project"
    }
  ]
}
statestring
  • added — resolves for the project, you do not have it
  • updated — you have it at a different sha
  • removed — you have it, it no longer resolves

Sending local={} therefore reports every skill as added, which is the correct answer for an empty working copy.

local must be a JSON object

A JSON array, or anything unparseable, is a 400 — not a 422, and not a silently empty comparison:

json
{ "detail": "`local` must be a JSON object of {name: sha}" }

Remember to URL-encode it. --data-urlencode above does this for you.

Errors

Shared by every endpoint on this page.

StatusWhenNotes
400diff only — local is not a JSON objectthe only 400 on this surface
401key missing, invalid or revoked
403the owner is not a member of that project
404the name does not resolve for this project, or the sha belongs to another skill

A project that does not exist returns 403, not 404

Membership is checked before the project is resolved, and you cannot be a member of a project that is not there. So a typo in project_id reads as "you may not look" rather than "no such project" — check the id before assuming an access problem.

Next

Copyright © 2026 elsai foundry.