---
title: "Publish a playlist"
method: POST
path: "/playlists/{id}/publish"
tags: ["playlists"]
---

# Publish a playlist

`POST /playlists/{id}/publish`

Publish the draft playlist as the new LIVE snapshot. Always async
(ADR 0003): the resolver marks StreamState.processing_state as
processing, signals PreparePlaylistWorkflow, and returns. Editor
polls Playlist.publishStatus to observe the PREPARING -> IDLE /
FAILED transition. Top-level errors carry extensions.code one of:
STALE_DRAFT, EMPTY_PLAYLIST, BROKEN_ITEM, RECIPE_MISMATCH,
NOT_LIVE, NOT_FOUND, INTERNAL.

## Path parameters

- `id` string, required

## Headers

- `Idempotency-Key` string

## Request body

- PublishPlaylistRequest
  - `expectedDraftVersion` integer, required — Required (asymmetric vs ApplyPlaylistOperationsInput.basedOnVersion, which is optional). Publish is the ONLY user-action that mutates runtime state, so editor-side OCC must be deliberate (NB-07). On STALE_DRAFT the editor refetches Playlist { version diff } using the returned currentVersion and retries.
  - `playlistId` string, required

## Response `200`

Successful response.

- Playlist
  - `copiedFrom` string, nullable
  - `createdAt` string, date-time, required
  - `diff` object, required — Snapshot-vs-draft diff (B-13 first-occurrence-by-objectId algorithm; zero-distance items go in `unchanged`, not `moved`; duplicate objectIds matched positionally). When no snapshot exists, diff = { unchanged: items, added: [], removed: [], moved: [] }.
    - `added` object[], required
      - `id` string, required
      - `objectID` string, required
      - `position` integer, required — 0-based; positions for a playlist form {0..N-1} exactly. Enforced by mutation layer (full-rewrite) and unique index.
    - `moved` object[], required
      - `fromPosition` integer, required
      - `item` object, required
        - `id` string, required
        - `objectID` string, required
        - `position` integer, required — 0-based; positions for a playlist form {0..N-1} exactly. Enforced by mutation layer (full-rewrite) and unique index.
      - `toPosition` integer, required
    - `removed` object[], required — May reference deleted PlaylistItem rows; projection-only type avoids the non-null hazard (NB-12).
      - `itemId` string, required — typeid plitm_* — frozen at write; underlying PlaylistItem row may have been deleted.
      - `objectId` string, required — typeid obj_*
      - `position` integer, required
    - `unchanged` object[], required
      - `id` string, required
      - `objectID` string, required
      - `position` integer, required — 0-based; positions for a playlist form {0..N-1} exactly. Enforced by mutation layer (full-rewrite) and unique index.
  - `hasUnpublishedChanges` boolean, required — snapshot != nil && Playlist.version > snapshot.version. Returns false during the B-11 STREAMING+no-snapshot window — deliberate: the stream is live but nothing has been published yet.
  - `id` string, required
  - `items` object[]
    - `id` string, required
    - `object` object, required
      - `duration` integer, nullable
      - `id` string, required
      - `name` string, required
      - `type` 'audio' | 'video' | 'image', required
    - `objectID` string, required
    - `position` integer, required — 0-based; positions for a playlist form {0..N-1} exactly. Enforced by mutation layer (full-rewrite) and unique index.
  - `kind` 'video' | 'audio', required
  - `liveItems` object[], required — Projected from PlaylistSnapshot.items JSON (NB-12) — never FK-hydrated against the live PlaylistItem table. Empty list during the B-11 STREAMING+no-snapshot window.
    - `itemId` string, required — typeid plitm_* — frozen at write; underlying PlaylistItem row may have been deleted.
    - `objectId` string, required — typeid obj_*
    - `position` integer, required
  - `liveVersion` integer, required — PlaylistSnapshot.version — Playlist.version frozen at the last LIVE write. Returns 0 when no snapshot exists OR during the B-11 STREAMING+no-snapshot transient window (WriteInitialPlaylistSnapshot retry).
  - `mode` 'sequential' | 'shuffle', required
  - `publishStatus` 'IDLE' | 'PENDING' | 'PREPARING' | 'FAILED' | 'NOT_LIVE', required — Workflow-state derivation. NB-13: PREPARING wins over PENDING even when a concurrent edit advances Playlist.version while a publish is preparing.
  - `streamID` string, required
  - `updatedAt` string, date-time, required
  - `version` integer, required — Bumped by 1 per committed spec mutation batch. Used for optimistic concurrency and as the snapshot handle for PreparePlaylistWorkflow.

## Other responses

- `401` — Missing or invalid access token.
- `403` — The token lacks the required scope or access to the resource.
- `404` — The resource does not exist or is not visible to the caller.
- `422` — The request body or parameters are invalid.
- `429` — Rate limit exceeded. Retry after the indicated delay.
- `502` — The upstream data service returned an error.

---

[API](https://skmtc.dev/streamloop/apis/streamloop-api.md) · [All operations](https://skmtc.dev/streamloop/apis/streamloop-api/llms.txt) · [OpenAPI document](https://skmtc-service-production.skmtc.workers.dev/v1/apis/streamloop/streamloop-api/revisions/7f30ccbac1b1/schema)
