---
title: "Unpublish a skill from the global catalog"
method: POST
path: "/api/v1/library/{owner}/{name}/unpublish"
tags: ["Library"]
---

# Unpublish a skill from the global catalog

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

## Request

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

Transitions a skill in the caller's account from `global` to `private` visibility, removing it from the public catalog.

**Subscriber side effects** (locked decision from #1446):
- Subscriber library rows are NOT deleted. Each subscriber   keeps their current copy of the skill on disk.
- Future bulk syncs naturally exclude the now-private skill   (per the existing visibility filter); subscribers stop   receiving future updates unless the publisher re-publishes.
- Each affected subscriber account's owner-role member(s)   receive an unpublish-notification email, subject to a 24h   debounce per `(skill, account)`. The publisher's own account   is excluded from notifications. The `notifiedSubscriberCount`   field reports the number of subscriber accounts notified   after debounce + suspension filtering.

**Idempotent.** Calling unpublish on an already-private skill returns **200** with `action: "unchanged"` rather than a 409. No subscriber emails are scheduled on the `unchanged` path (no transition happened).

**Permission gate.** Same as `/publish` — admin+ by default, plus the per-member `canPublish` entitlement. The `canPublish` capability gates BOTH publish AND unpublish symmetrically.

**Idempotency.** Optional `Idempotency-Key` header: 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`.

Rate-limit bucket: `cli-crud` (60/min per key).

## 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 the skill was unpublished (`action: "unpublished"`, with `notifiedSubscriberCount` reflecting the number of subscriber accounts that received an email after debounce + suspension filtering, and excluding the publisher's own account) or it was already private (`action: "unchanged"`, `notifiedSubscriberCount: 0`). `skill` carries the full post-transition SyncSkill so the client can update its local cache. On Idempotency-Key replay the same body is returned with `Idempotent-Replayed: true`.

- LibraryUnpublishResponse: Response returned by `POST /api/v1/library/{owner}/{name}/unpublish` (#1449). `skill` carries the full post-flip SyncSkill so clients can update their local cache. `notifiedSubscriberCount` lets the publisher confirm how many subscriber accounts the unpublish reached after debounce/suspension filtering.
  - `action` 'unpublished' | 'unchanged', required: Outcome of an unpublish call. `unpublished` = visibility transitioned from `global` to `private`. `unchanged` = the skill was already private; no transition happened (idempotent retry).
  - `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`.
  - `notifiedSubscriberCount` integer, required: Number of OTHER accounts whose owner-role members were scheduled to receive an unpublish-notification email. Filters: excludes the publisher's own account; excludes accounts in the 24h debounce window from a prior unpublish on this skill; excludes suspended accounts and suspended owner-memberships. Zero on `unchanged` (no transition).

## Other responses

- `400`: Request validation failure. `code: idempotency_key_too_long` when the Idempotency-Key header exceeds 255 characters.
- `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 unpublishing against another account's skill. Both cases return the same 404 by design — info-hiding per the same rule as `GET /api/v1/library/{owner}/{name}`. `code` is `skill_not_found`.
- `409`: Concurrent Idempotency-Key replay while the first request is still in-flight (`code: idempotency_key_in_progress`). Retry after a short backoff.
- `422`: Idempotency-Key replay carried a different body than the original (`code: idempotency_key_reused`). Unlike publish, unpublish has no product-rule preconditions of its own.
- `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)
