---
title: "Release a new version with an explicit label"
method: POST
path: "/api/v1/library/{owner}/{name}/versions"
tags: ["Library"]
---

# Release a new version with an explicit label

`POST /api/v1/library/{owner}/{name}/versions`

## Request

- Base URL: `https://skillrepo.dev`
- URL: `https://skillrepo.dev/api/v1/library/{owner}/{name}/versions`
- Auth: HTTP bearer

Power-user override for the auto-bump in `POST /api/v1/library`. Releases a new `skill_versions` row for an existing skill with the caller-supplied `version` label, bypassing the server's default `N+1.0` / `major.N+1` classification.

**When to use this.** Most callers should use `POST /library` (multipart upsert) — that endpoint creates the skill on first push and releases new versions thereafter with an auto-classified label. Reach for `/versions` only when:
- You are importing a skill with pre-existing external version   history (e.g., `2.3.7`) and want the new release to keep that   numbering.
- You coordinate version labels with an external system   (e.g., a skill that mirrors a tool's own versions).
- You want a human-authored `changelog` string instead of the   auto-generated `Contract changed: ...` text.

**Update-only.** The skill must already exist in the caller's account (404 `skill_not_found` otherwise). To create a new skill, use `POST /api/v1/library`.

**Owner-addressed.** `{owner}` is the skill's published attribution. For a skill you CLAIMED whose attribution differs from your account slug (e.g. releasing `anthropics/my-skill` from an account slugged `alice`), address it by that published attribution — the release updates the skill you own at that path, and the response echoes the attribution as `owner`. `POST /api/v1/library` differs: it is name-addressed and always targets your own namespace.

**Unchanged content.** If the SKILL.md + files are byte-identical to the current version, the response is **200** with `action: "unchanged"` and the current `skill` state — no new version is written and the caller-supplied `version` label is ignored. Same shape as `POST /library` for an unchanged push, so retries from CI scripts that don't bother editing first succeed idempotently rather than failing with a build-breaking 4xx (#1490, parity with `POST /library`).

**Version label format.** 1-64 characters, must start with an alphanumeric, body may include `.`, `-`, `+`. Permits semver, semver pre-release tags, build metadata, and plain labels. No monotonicity check — `3.0.0-rc1` after `2.3.7` is accepted. Duplicate labels for the same skill return 409 `version_conflict`.

**File-path encoding.** Same as `POST /library`: each file part's in-skill relative path travels via the multipart `Content-Disposition: filename` parameter (RFC 7578). Root `SKILL.md` is required and identified by exact-match `filename=SKILL.md`.

**Payload size.** Total multipart body capped at 4.5 MB.

**Idempotency.** Supports the optional `Idempotency-Key` header. Same semantics as `POST /library` — retries with the same key + same body return the cached response with `Idempotent-Replayed: true`; retries with a different body return 422 `idempotency_key_reused`.

Error codes: `missing_version`, `invalid_version`, `missing_skill_md`, `invalid_skill_md`, `invalid_path`, `duplicate_path`, `invalid_multipart`, `name_mismatch`, `payload_too_large`, `skill_not_found`, `version_conflict`, `source_conflict`, `rate_limited`, `idempotency_key_too_long`, `idempotency_key_in_progress`, `idempotency_key_reused`.

## Path parameters

- `owner` string, required
- `name` string, required

## Headers

- `Idempotency-Key` string: Client-supplied idempotency key (max 255 chars). Retries with the same key + same request body return the cached response (with `Idempotent-Replayed: true`) instead of re-executing the handler. Retries with the same key + a different body return 422 `idempotency_key_reused`. Concurrent retries while the original is in-flight return 409 `idempotency_key_in_progress`. Cached responses live for 24 hours.

## Response `200`

Either a new version was released (`action: "updated"`, `skill.version` carries the caller-supplied label) or the uploaded content was byte-identical to the current version (`action: "unchanged"`, `skill` echoes the current state, `bump` is null, no new row was written). The unchanged outcome mirrors `POST /library` for parity (#1490) so idempotent retries from CI tooling don't break builds. On Idempotency-Key replays the same body is returned with `Idempotent-Replayed: true`.

- LibraryVersionReleaseResponse: Response returned by `POST /api/v1/library/{owner}/{name}/versions` (#1450, power-user explicit-label release). `skill.version` is the caller-supplied label, not the server-computed bump label. When server-side safety analysis is enabled, `skill` reflects the JUST-PUSHED state immediately, while `GET /api/v1/library` continues to surface the prior `currentVersion` until analysis completes — same optimistic-response model as `POST /library`.
  - `action` 'updated' | 'unchanged', required: Server-detected outcome. `updated` = a new `skill_versions` row was released with the caller-supplied `version` label. `unchanged` = uploaded SKILL.md + files were byte-identical to the current version; no new row was written and the response echoes the current state. `created` is unreachable — the endpoint requires the skill already exist (404 otherwise).
  - `bump` 'major' | 'minor', nullable, required: Server-classified bump from the contract-field diff (`description`, `allowed-tools`, `compatibility`) on `updated`. The label on the new `skill_versions` row is the caller-supplied `version`, NOT the server-computed bump label — `bump` is informational about the scope of contract change for clients that want to surface it. Null on `unchanged`.
  - `skill` SyncSkill, required: Per-skill shape shared by bulk library sync and single-skill GET. Content is byte-identical across endpoints; the optional `governance` field rides only the manifest/scoped read paths.
    - `owner` string, required
    - `ownerName` string, required
    - `name` string, required
    - `version` string, nullable, required
    - `description` string, required
    - `keywords` string[], required: Single-word keywords extracted from the skill's `triggers` (stop-words removed, lowercased) for client-side indexing.
    - `updatedAt` string, date-time, required
    - `etag` string, required: Per-skill ETag of the form `"owner/name@<timestamp>"`. Used by clients for conditional re-fetching; the bulk sync response also sets an HTTP-level `ETag` covering the whole response.
    - `contextSignals` ContextSignals, nullable, required: Context-signals object attached to a skill by the analysis pipeline. Open-shaped — clients should pass through to UI without strict validation. Null when analysis has not yet run.
    - `files` SyncFile[], required
      - `path` string, required: File path relative to the skill root.
      - `content` string, required: Inlined file contents. UTF-8 when `encoding: utf-8`; base64 of the raw bytes when `encoding: base64`.
      - `sha256` string, required: Hex-encoded SHA-256 of the raw file bytes. Empty string for the synthesized SKILL.md entry (clients use the version-level `etag` for freshness there).
      - `contentType` string, required: MIME type. Text content types are inlined as UTF-8; everything else as base64.
      - `size` integer, required: File size in bytes (pre-base64-encoding).
      - `encoding` 'utf-8' | 'base64', required: How `content` is encoded. `utf-8` for text types, `base64` for binary.
    - `filesIncomplete` boolean, required: True when one or more blob-backed files failed to inline and were dropped from `files`. When true, the route SUPPRESSES the response-level HTTP `ETag` header and sets `Cache-Control: private, no-store` so clients re-fetch on the next sync instead of caching an incomplete response. On the push (POST) response this field is almost always `false` — files were just uploaded successfully, so the inline-fetch rarely fails. A `true` value here indicates a transient blob store hiccup between upload and response build; the next GET will return the complete state.
    - `governance` SyncSkillGovernance: Per-skill delivery governance carried ONLY on the non-recording read paths — the `?manifest=1` whole-library and skillset-scoped reads (`skillrepo list`). Absent on the byte-identical whole-library sync wire (D2) and the skillset-scoped sync wire, and absent entirely when the recall feature is disabled.
      - `status` 'latest' | 'serving' | 'pinned', required: Delivery status in the shared version-status vocabulary: `latest` — the served version is the publisher's head (healthy); `serving` — a recall rolled the account back to an earlier version (the served version is below head and diverted); `pinned` — a deliberate hold behind head (not a recall).
      - `head` string, nullable, required: The publisher's current head version label, for context (e.g. "vX available"). Null when the skill has no published head.
      - `recalledVersion` string, nullable: Present only when `status` is `serving`: the version that WOULD serve absent the recall — the one recalled and rolled back FROM. The version actually served is this skill's own `version`.

## Other responses

- `400`: Request validation failure (the request is malformed in some way). `code` distinguishes the cause: `missing_version`, `invalid_version`, `missing_skill_md`, `invalid_skill_md`, `invalid_path`, `duplicate_path`, `invalid_multipart`, `name_mismatch`, `idempotency_key_too_long`.
- `401`: Missing or invalid authentication. For routes that require `bearerAccessKey` or `bearerCatalogKey`.
- `403`: Authenticated, but the key lacks the required scope or the account is suspended. Use `code` to distinguish: `insufficient_scope`, `account_suspended`, `plan_limit`.
- `404`: Skill does not exist in the caller's account, OR the caller is trying to release a version against another account's skill. Both cases return the same 404 by design — the endpoint cannot be used to probe whether a skill exists on another account (same cross-account info-hiding rule as `GET /api/v1/library/{owner}/{name}`). Use `POST /api/v1/library` to create your own skill. The 404 `code` is `skill_not_found` on the documented path. An unreachable defense-in-depth case (`account_not_found`) also returns 404 if the authenticated account row goes missing between auth and the multipart helper; that is a server invariant violation, not a client-recoverable error.
- `409`: Conflict. `version_conflict` — either the supplied label is already used by another version of this skill, or a concurrent release for this skill landed first (the label itself may still be available; retry). `source_conflict` — the existing skill is attributed under a different source type (GitHub vs manual). `idempotency_key_in_progress` — concurrent retry while the first request is in-flight.
- `413`: Multipart body exceeds the 4.5 MB cap.
- `422`: Request was syntactically valid but cannot be processed in the current state. `idempotency_key_reused` — same key sent with a different request body. (Note: prior to #1490, byte-identical content also returned 422 `nothing_to_release`. That contract was relaxed to 200 `unchanged` for parity with `POST /library`.)
- `429`: Rate limit exceeded on the route's bucket. Includes `X-RateLimit-*` headers indicating the limit, remaining, and reset time.

---

[API](https://skmtc.dev/skillrepo/apis/skillrepo-api.md) · [All operations](https://skmtc.dev/skillrepo/apis/skillrepo-api/llms.txt) · [OpenAPI document](https://skmtc.dev/skillrepo/apis/skillrepo-api/revisions/b62bac246918?raw)
