# impartire API — Reference: Playlists

Every path is under `https://impartire.com/api/v1`. The machine-readable contract is https://impartire.com/api/v1/openapi.json (OpenAPI 3.1).

#### GET /api/v1/playlists

List a project's playlists

`project_id` is required: playlists are listed one project at a time. Ordered by name unless `sort` says otherwise. To poll for changes, pass `updated_after` with the newest `updated_at` you have seen and `sort=-updated_at` (or `updated_at`); `updated_at` moves when the name, description or target_count change, not when items do. A deleted playlist leaves the list. An OAuth token's person needs read on the project (a project they cannot open is a 422 like a missing one).

Scopes: `projects:read`

Parameters:

- `project_id` (query, string (uuid), required) — Required: the project whose playlists to list (GET /api/v1/projects). Not its sub-projects.
- `updated_after` (query, string (date-time)) — Only playlists changed strictly after this instant (ISO 8601 with offset), compared to the millisecond — pass the newest `updated_at` you have seen and that playlist is not returned again.
- `sort` (query, "name" | "-name" | "updated_at" | "-updated_at") — Sort key; a leading `-` is descending. Default `name`. Ties break on id.
- `limit` (query, integer) — Page size, default 50. Values above 100 are treated as 100.
- `cursor` (query, string) — next_cursor from the previous page, with the other parameters unchanged.

Response 200 — `PlaylistList`

- `data` (array of Playlist, required)
  - `id` (string (uuid), required)
  - `project` (ProjectRef, required) — The project the playlist belongs to — its permission parent: who may open the project may open the playlist, who may update the project may change it.
    - `id` (string (uuid), required)
    - `name` (string, required)
  - `name` (string, required)
  - `description` (string or null, required)
  - `target_count` (integer or null, required) — How many items the playlist is meant to hold (e.g. a season's episode count), or null. Advisory: nothing refuses an item past it.
  - `item_count` (integer, required) — How many items it holds (GET /playlists/{id}/items lists them).
  - `created_at` (string (date-time), required)
  - `updated_at` (string (date-time), required) — When its name, description or target_count last changed. Adding, removing or reordering items does not move it (the ETag moves instead).
- `next_cursor` (string or null, required) — Pass as `cursor` to get the next page; null means this is the last page.

Other responses:

- 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one.
- 403 (Problem) — insufficient-scope — the key lacks a required scope.
- 404 (Problem) — not-found.
- 422 (Problem) — validation — a missing or unknown `project_id`, or a bad `limit`, `updated_after`, `sort` or `cursor`.
- 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds.
- 500 (Problem) — internal.

#### POST /api/v1/playlists

Create a playlist in a project

A typed project's type must allow playlists (an untyped project always does); otherwise 422 `project-type-disallows`. An OAuth token's person needs update on the project.

Scopes: `projects:write`

Parameters:

- `Idempotency-Key` (header, string) — Makes a retry safe: the same key and request within 24 hours replays the first response. 1–255 characters.

Request body — `CreatePlaylistRequest`

- `project_id` (string (uuid), required) — The project it belongs to (GET /api/v1/projects). A typed project's type must allow playlists.
- `name` (string, required)
- `description` (string or null)
- `target_count` (integer or null) — How many items it is meant to hold; advisory.

Response 201 — `Playlist`: An ordered, count-aware sequence of assets belonging to a project (e.g. a season's episodes).

- `id` (string (uuid), required)
- `project` (ProjectRef, required) — The project the playlist belongs to — its permission parent: who may open the project may open the playlist, who may update the project may change it.
  - `id` (string (uuid), required)
  - `name` (string, required)
- `name` (string, required)
- `description` (string or null, required)
- `target_count` (integer or null, required) — How many items the playlist is meant to hold (e.g. a season's episode count), or null. Advisory: nothing refuses an item past it.
- `item_count` (integer, required) — How many items it holds (GET /playlists/{id}/items lists them).
- `created_at` (string (date-time), required)
- `updated_at` (string (date-time), required) — When its name, description or target_count last changed. Adding, removing or reordering items does not move it (the ETag moves instead).

Other responses:

- 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one.
- 403 (Problem) — insufficient-scope, or insufficient-permission — an OAuth token's person may open the project but not update it.
- 404 (Problem) — not-found.
- 409 (Problem) — idempotency-in-flight.
- 422 (Problem) — project-type-disallows, validation (project_id, name, description, target_count), or idempotency-mismatch.
- 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds.
- 500 (Problem) — internal.

#### GET /api/v1/playlists/{id}

Get one playlist

Scopes: `projects:read`

Parameters:

- `id` (path, string (uuid), required) — The playlist's id, from GET /api/v1/playlists?project_id=.
- `If-None-Match` (header, string) — An ETag from a previous response; 304 if unchanged.

Response 200 — `Playlist`: An ordered, count-aware sequence of assets belonging to a project (e.g. a season's episodes).

- `id` (string (uuid), required)
- `project` (ProjectRef, required) — The project the playlist belongs to — its permission parent: who may open the project may open the playlist, who may update the project may change it.
  - `id` (string (uuid), required)
  - `name` (string, required)
- `name` (string, required)
- `description` (string or null, required)
- `target_count` (integer or null, required) — How many items the playlist is meant to hold (e.g. a season's episode count), or null. Advisory: nothing refuses an item past it.
- `item_count` (integer, required) — How many items it holds (GET /playlists/{id}/items lists them).
- `created_at` (string (date-time), required)
- `updated_at` (string (date-time), required) — When its name, description or target_count last changed. Adding, removing or reordering items does not move it (the ETag moves instead).

Other responses:

- 304 — Not modified — the ETag in If-None-Match is current.
- 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one.
- 403 (Problem) — insufficient-scope — the key lacks a required scope.
- 404 (Problem) — not-found.
- 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds.
- 500 (Problem) — internal.

#### PATCH /api/v1/playlists/{id}

Rename or describe a playlist, or change its target count

If-Match is required: send the ETag from the last GET (or from the create). An unknown field is a 422; moving a playlist to another project is not in v1. An OAuth token's person needs update on the playlist's project.

Scopes: `projects:write`

Parameters:

- `id` (path, string (uuid), required) — The playlist's id, from GET /api/v1/playlists?project_id=.
- `If-Match` (header, string, required) — The playlist's ETag.

Request body — `UpdatePlaylistRequest`: At least one of name, description, target_count. Moving a playlist to another project is not in v1; an unknown field is a 422.

- `name` (string)
- `description` (string or null) — null clears it.
- `target_count` (integer or null) — null clears it.

Response 200 — `Playlist`: An ordered, count-aware sequence of assets belonging to a project (e.g. a season's episodes).

- `id` (string (uuid), required)
- `project` (ProjectRef, required) — The project the playlist belongs to — its permission parent: who may open the project may open the playlist, who may update the project may change it.
  - `id` (string (uuid), required)
  - `name` (string, required)
- `name` (string, required)
- `description` (string or null, required)
- `target_count` (integer or null, required) — How many items the playlist is meant to hold (e.g. a season's episode count), or null. Advisory: nothing refuses an item past it.
- `item_count` (integer, required) — How many items it holds (GET /playlists/{id}/items lists them).
- `created_at` (string (date-time), required)
- `updated_at` (string (date-time), required) — When its name, description or target_count last changed. Adding, removing or reordering items does not move it (the ETag moves instead).

Other responses:

- 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one.
- 403 (Problem) — insufficient-scope, or insufficient-permission — an OAuth token's person may open the project but not update it.
- 404 (Problem) — not-found.
- 412 (Problem) — precondition-failed — If-Match is stale; `etag` in the body is the current one.
- 422 (Problem) — validation (empty body, an unknown field, name, description, target_count).
- 428 (Problem) — precondition-required — no If-Match.
- 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds.
- 500 (Problem) — internal.

#### DELETE /api/v1/playlists/{id}

Delete a playlist

Soft-deletes the playlist; it no longer appears in reads or lists. Its assets are untouched. No If-Match: a second DELETE is a 404. An OAuth token's person needs update on the playlist's project.

Scopes: `projects:write`

Parameters:

- `id` (path, string (uuid), required) — The playlist's id, from GET /api/v1/playlists?project_id=.

Other responses:

- 204 — Deleted.
- 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one.
- 403 (Problem) — insufficient-scope, or insufficient-permission — an OAuth token's person may open the project but not update it.
- 404 (Problem) — not-found.
- 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds.
- 500 (Problem) — internal.

#### GET /api/v1/playlists/{id}/items

List a playlist's items in order

Ordered by `sort_order`, then by when each was added. Paginated: the list is bounded by the playlist, a page at a time. Each item names its asset's current version. For an OAuth token, an item whose asset its person may not read keeps its place and id but is `restricted` with `asset: null`.

Scopes: `projects:read`

Parameters:

- `id` (path, string (uuid), required) — The playlist's id, from GET /api/v1/playlists?project_id=.
- `limit` (query, integer) — Page size, default 50. Values above 100 are treated as 100.
- `cursor` (query, string) — next_cursor from the previous page.

Response 200 — `PlaylistItemList`

- `data` (array of PlaylistItem, required)
  - `id` (string (uuid), required) — The item's id — what DELETE …/items/{item_id} and PUT …/items/order take.
  - `asset` (object or null, required) — The asset (its current version). Null when `restricted`.
    - `id` (string (uuid), required)
    - `name` (string, required)
    - `type` (object, required)
      - `id` (string (uuid), required)
      - `name` (string, required)
  - `restricted` (boolean, required) — True for an OAuth token whose person may not read the asset (a playlist in an open project may hold material from a private one): the item keeps its place and id but not the asset's identity. Always false for an API key.
  - `sort_order` (integer, required) — The item's place in the list; items are listed by it, ascending.
  - `position` (integer or null, required) — An optional editorial slot number (e.g. episode 3), independent of the list order.
  - `added_at` (string (date-time), required)
- `next_cursor` (string or null, required) — Pass as `cursor` to get the next page; null means this is the last page.

Other responses:

- 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one.
- 403 (Problem) — insufficient-scope — the key lacks a required scope.
- 404 (Problem) — not-found.
- 422 (Problem) — validation — a bad `limit` or `cursor`.
- 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds.
- 500 (Problem) — internal.

#### POST /api/v1/playlists/{id}/items

Append an asset to a playlist

Pins the asset's current version and appends it after the last item; `position` is an optional editorial slot number, independent of the order. An asset already in the playlist (any version of it) is 409 `already-member`. An OAuth token's person needs update on the playlist's project and read on the asset.

Scopes: `projects:write`

Parameters:

- `id` (path, string (uuid), required) — The playlist's id, from GET /api/v1/playlists?project_id=.
- `Idempotency-Key` (header, string) — Makes a retry safe: the same key and request within 24 hours replays the first response. 1–255 characters.

Request body — `AddPlaylistItemRequest`

- `asset_id` (string (uuid), required) — The asset to add (its current version's id or its root id; the item pins the current version). GET /api/v1/assets lists them.
- `position` (integer or null) — An editorial slot number (e.g. episode 3). The item is appended to the list whatever its position.

Response 201 — `PlaylistItem`: One item of a playlist: an asset at a place in the list.

- `id` (string (uuid), required) — The item's id — what DELETE …/items/{item_id} and PUT …/items/order take.
- `asset` (object or null, required) — The asset (its current version). Null when `restricted`.
  - `id` (string (uuid), required)
  - `name` (string, required)
  - `type` (object, required)
    - `id` (string (uuid), required)
    - `name` (string, required)
- `restricted` (boolean, required) — True for an OAuth token whose person may not read the asset (a playlist in an open project may hold material from a private one): the item keeps its place and id but not the asset's identity. Always false for an API key.
- `sort_order` (integer, required) — The item's place in the list; items are listed by it, ascending.
- `position` (integer or null, required) — An optional editorial slot number (e.g. episode 3), independent of the list order.
- `added_at` (string (date-time), required)

Other responses:

- 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one.
- 403 (Problem) — insufficient-scope, or insufficient-permission — an OAuth token's person may open the project but not update it.
- 404 (Problem) — not-found.
- 409 (Problem) — already-member — the asset is already in the playlist (`item_id` in the body); or idempotency-in-flight.
- 422 (Problem) — unknown-asset, validation (asset_id, position), or idempotency-mismatch.
- 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds.
- 500 (Problem) — internal.

#### DELETE /api/v1/playlists/{id}/items/{item_id}

Remove an item from a playlist

The asset itself is untouched; the other items keep their `sort_order`. A second DELETE is a 404. An OAuth token's person needs update on the playlist's project.

Scopes: `projects:write`

Parameters:

- `id` (path, string (uuid), required) — The playlist's id, from GET /api/v1/playlists?project_id=.
- `item_id` (path, string (uuid), required) — The item's id, from GET /api/v1/playlists/{id}/items.

Other responses:

- 204 — Removed.
- 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one.
- 403 (Problem) — insufficient-scope, or insufficient-permission — an OAuth token's person may open the project but not update it.
- 404 (Problem) — not-found.
- 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds.
- 500 (Problem) — internal.

#### PUT /api/v1/playlists/{id}/items/order

Put a playlist's items in a new order

`item_ids` is every item of the playlist, each exactly once, in the new order; their `sort_order` becomes 0, 1, 2… (`position` is untouched). Anything else (a missing, repeated or foreign id) is a 422. A playlist of more than 1000 items is reordered in the app. Answers the whole list after. If an item was added or removed meanwhile, nothing moves and the answer is 409 `conflict`: read the items and send the order again. Safe to repeat: no Idempotency-Key, no If-Match. An OAuth token's person needs update on the playlist's project.

Scopes: `projects:write`

Parameters:

- `id` (path, string (uuid), required) — The playlist's id, from GET /api/v1/playlists?project_id=.

Request body — `ReorderPlaylistItemsRequest`

- `item_ids` (array of string (uuid), required) — Every item id of the playlist, each exactly once, in the new order (at most 1000). The items' sort_order becomes 0, 1, 2… in this order; position is untouched.

Response 200 — `PlaylistItemList`

- `data` (array of PlaylistItem, required)
  - `id` (string (uuid), required) — The item's id — what DELETE …/items/{item_id} and PUT …/items/order take.
  - `asset` (object or null, required) — The asset (its current version). Null when `restricted`.
    - `id` (string (uuid), required)
    - `name` (string, required)
    - `type` (object, required)
      - `id` (string (uuid), required)
      - `name` (string, required)
  - `restricted` (boolean, required) — True for an OAuth token whose person may not read the asset (a playlist in an open project may hold material from a private one): the item keeps its place and id but not the asset's identity. Always false for an API key.
  - `sort_order` (integer, required) — The item's place in the list; items are listed by it, ascending.
  - `position` (integer or null, required) — An optional editorial slot number (e.g. episode 3), independent of the list order.
  - `added_at` (string (date-time), required)
- `next_cursor` (string or null, required) — Pass as `cursor` to get the next page; null means this is the last page.

Other responses:

- 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one.
- 403 (Problem) — insufficient-scope, or insufficient-permission — an OAuth token's person may open the project but not update it.
- 404 (Problem) — not-found.
- 409 (Problem) — conflict — the playlist's items changed while it was being reordered; nothing moved.
- 422 (Problem) — validation — `item_ids` is not every item exactly once, or the playlist is too large to reorder here.
- 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds.
- 500 (Problem) — internal.
