Skills

Skills API

Create, evaluate, publish, and adopt agent skills, and manage the rubric config that gates publishing.

Base path: /api/v1/skills. See the Skill Marketplace concept page for the lifecycle and object model, and the Eval Results API for external verdict ingestion.

Entitlement and roles

Requires the skill_marketplace entitlement (Enterprise plan). Reads need the viewer role; writes need member.

Endpoints

Routes are matched in this order (the public shelf and rubric routes are registered before :id so they aren't swallowed by it):

GET
/api/v1/skills/public

Browse the public shelf (cross-org).

GET
/api/v1/skills/public/:id

Get a public shelf skill.

POST
/api/v1/skills/public/:id/adopt

Copy a public skill into your org.

GET
/api/v1/skills/rubric

Get (or seed) your org's rubric config.

PUT
/api/v1/skills/rubric/:id

Update your org's rubric config.

POST
/api/v1/skills

Create a skill.

GET
/api/v1/skills

List your org's skills.

GET
/api/v1/skills/:id

Get a skill.

PUT
/api/v1/skills/:id

Update a skill.

DELETE
/api/v1/skills/:id

Delete a skill.

POST
/api/v1/skills/:id/publish

Publish to the public shelf (gated).

POST
/api/v1/skills/:id/unpublish

Unpublish.

POST
/api/v1/skills/:id/evaluate

Force an out-of-band re-evaluation.

GET
/api/v1/skills/:id/evaluations

List a skill's evaluation history.

Create

Either supply raw_source (frontmatter + body, parsed as-is) or discrete fields (name, description required; headline, body optional), which get synthesized into that shape.

bash
curl -X POST https://api.ingateai.com/api/v1/skills \
  -H "X-Ingate-Key: sk-ingate-your-key" \
  -H "Content-Type: application/json" \
  -d '{
    "raw_source": "---\nname: pdf-summarizer\nheadline: Summarize a PDF\ndescription: Extracts text and produces a concise summary\n---\n## Steps\n1. Extract text\n2. Summarize"
  }'

Response 201: the created Skill, version: 1, visibility: "private". Creation also dispatches an async evaluation against the org's rubric.

Create Errors

StatusError codeCause
400invalid_bodyMalformed JSON
400missing_requiredraw_source absent and name/description also absent
400invalid_frontmatterFrontmatter fails validation (missing name/description, non-kebab-case name, malformed YAML)
500internal_frontmatter_synthesisSynthesizing frontmatter from discrete fields failed unexpectedly

List

bash
curl "https://api.ingateai.com/api/v1/skills?visibility=private&name_like=pdf&limit=50" \
  -H "X-Ingate-Key: sk-ingate-your-key"
ParamTypeDescription
visibilitystringprivate or public; empty = all
name_likestringSubstring match on name
limitintMax results

Response 200: {"skills": [ ... ]}, the caller's own org only.

Get, Update, Delete

GET /api/v1/skills/:id returns the Skill. PUT /api/v1/skills/:id is a partial update accepting any of raw_source, name, headline, description, body; if raw_source is supplied it is re-parsed and wins over any other field in the same request. The update response is the updated Skill (re-fetched after write, so the version and timestamps reflect the new state); it also logs a new SkillVersion snapshot and dispatches re-evaluation. DELETE /api/v1/skills/:id returns {"deleted": true}.

Publish

Preflight: the skill's current version must have a passing evaluation from one of the org's allowed_verdict_sources (rubric config; defaults to ["internal"], Ingate's own judge only). Publishing an already-public skill is idempotent and returns 200, not 409.

jsonResponse (200 OK)
{"published_at": "2026-08-29T12:00:00Z"}

409 not_publishable, no passing evaluation for the current version from an allowed source:

jsonResponse (409 Conflict)
{
  "error": "publish requires a passing evaluation for the current version",
  "error_code": "not_publishable",
  "skill_id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
  "version": 3
}

See the Eval Results API for how to let an external evaluator's verdict satisfy this gate.

Unpublish

Idempotent: unpublishing an already-private skill also returns 200. Response 200: {"unpublished": true}.

Force Evaluate

Dispatch an out-of-band re-evaluation regardless of the periodic scheduler's cadence.

jsonResponse (202 Accepted)
{"dispatched": true, "queued": true}

queued: false means the pipeline's dispatch buffer was full and the request was dropped. Retry later.

List Evaluations

bash
curl "https://api.ingateai.com/api/v1/skills/d290f1ee-6c54-4b01-90e6-d701748f0851/evaluations?limit=20" \
  -H "X-Ingate-Key: sk-ingate-your-key"

limit defaults to 50, capped at 200. Response 200: {"evaluations": [ ... ]},

json
{
  "id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
  "org_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "skill_id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
  "skill_version": 3,
  "rubric_config_id": "7c9e6...",
  "axes": {
    "name_clarity": {"score": 0.9, "rationale": "..."},
    "description_task_descriptiveness": {"score": 0.85, "rationale": "..."}
  },
  "overall_score": 0.88,
  "passed": true,
  "judge_provider": "openai",
  "judge_model": "gpt-4o-mini",
  "latency_ms": 842,
  "prompt_tokens": 512,
  "completion_tokens": 96,
  "source": "internal",
  "created_at": "2026-08-29T12:00:00Z"
}

source is "internal" for Ingate's built-in judge, or an external evaluator name (e.g. "promptfoo") for verdicts ingested via the Eval Results API. external_ref (omitted when empty) is an opaque pointer into that external system.

Public Shelf

Browse

bash
curl "https://api.ingateai.com/api/v1/skills/public?name_like=pdf&description_like=summar&limit=50" \
  -H "X-Ingate-Key: sk-ingate-your-key"
ParamTypeDescription
name_likestringSubstring match on name
description_likestringSubstring match on description
limitintMax results

Response 200: {"skills": [ ... ]} across all orgs' published skills.

Get

GET /api/v1/skills/public/:id returns the public Skill.

Adopt

Deep-copies a public shelf skill into the caller's org as a new, private skill (version: 1, source_skill_id set to the shelf entry). Renames on name collision (-adopted-1, -adopted-2, …).

jsonResponse (201 Created)
{
  "skill": { /* the new Skill */ },
  "renamed": false,
  "name": "pdf-summarizer"
}

Errors: 400 invalid_id, 404 not_found (source skill), 409 duplicate_name (no non-colliding name found after 100 attempts).

Rubric Config

Get (Seed on First Access)

GET /api/v1/skills/rubric seeds a default RubricConfig for the org from server-configured defaults if none exists yet. Response 200: the RubricConfig.

Update

Partial update: only provided fields change. :id is the config's id (from the GET response above). Accepts any of judge_provider, judge_model, embed_provider, embed_model, embed_dims, weights, pass_threshold, allowed_verdict_sources.

bash
curl -X PUT https://api.ingateai.com/api/v1/skills/rubric/7c9e6679-7425-40de-944b-e07fc1f90ae7 \
  -H "X-Ingate-Key: sk-ingate-your-key" \
  -H "Content-Type: application/json" \
  -d '{"allowed_verdict_sources": ["internal", "promptfoo"]}'

Response 200: the updated RubricConfig. Errors: 400 invalid_id, 400 invalid_body, 404 not_found.