---
title: "Create a project-owned publishing series."
method: POST
path: "/story-projects/{projectId}/publishing-series"
tags: ["Publishing"]
---

# Create a project-owned publishing series.

`POST /story-projects/{projectId}/publishing-series`

## Request

- Base URL: `https://docs.sleeperhit.studio/api/v1`
- URL: `https://docs.sleeperhit.studio/api/v1/story-projects/{projectId}/publishing-series`
- Auth: HTTP bearer

Creates the series: medium, format, destination metadata, and prompt direction. Audio series automatically get a first-party RSS destination. A show made from the series is its own resource: `/publishing-series/{seriesId}/show`.

## Headers

- `Idempotency-Key` string, required

## Request body

- PublishingSeriesCreateRequest
  - `title` string: Series title. When omitted, defaults to the project name.
  - `slug` string
  - `description` string
  - `medium` 'audio' | 'video' | 'short_form' | 'text' | 'mixed'
  - `format` string
  - `status` 'draft' | 'active' | 'paused' | 'archived'
  - `timezone` string
  - `language` string
  - `rating` 'clean' | 'explicit' | 'unknown'
  - `explicitContent` boolean
  - `podcastType` 'episodic' | 'serial'
  - `podcastCategory` string
  - `podcastSubcategory` string
  - `coverArtUrl` string, uri
  - `websiteUrl` string, uri
  - `copyright` string
  - `syntheticDisclosure` string: The show's synthetic-media line (legal gap G8a): it ends the feed's description and every episode's notes, is the feed's `podcast:txt` (purpose synthetic-media) and shows on the show's pages. Reword it; it can never be empty (default: "Voices and music in this show are synthetic.").
  - `ownerName` string
  - `ownerEmail` string
  - `promptDirection` string
  - `metadata` object
  - `visibility` 'public' | 'unlisted' | 'private': What the show's slug exposes: public (landing page, archive, and the podcast feed), unlisted (shared episode links and the podcast feed, reached by link: never indexed, and the feed asks podcast directories not to list it), or private (nothing; the feed answers 404). A draft show's feed is served too.
  - `standingApprovalKeyId` string, nullable: Grant the show's standing approval to ONE API key (its id, or `"self"` for the calling key; an OAuth connection or a CLI/app sign-in key cannot hold one, so use a named API key): the app holding it makes the show from then on, and a show Sleeper was making turns off. Null revokes it. Granting requires `userConfirmed: true`; audio series only.
  - `userConfirmed` true: Required when the request grants a standing approval (`standingApprovalKeyId` set), after the user explicitly agrees the app may approve and publish episodes without asking each time.

## Response `200`

Publishing series envelope.

- PublishingSeriesEnvelope
  - `series` PublishingSeries, required
    - `id` string, required
    - `projectId` string, required
    - `seriesBibleDocumentId` string, nullable
    - `title` string, required
    - `slug` string, required
    - `description` string, nullable
    - `medium` 'audio' | 'video' | 'short_form' | 'text' | 'mixed', required
    - `format` string, required
    - `status` 'draft' | 'active' | 'paused' | 'archived', required
    - `timezone` string
    - `language` string
    - `rating` 'clean' | 'explicit' | 'unknown'
    - `explicitContent` boolean
    - `podcastType` 'episodic' | 'serial'
    - `podcastCategory` string
    - `podcastSubcategory` string, nullable
    - `coverArtUrl` string, uri, nullable
    - `websiteUrl` string, uri, nullable
    - `promptDirection` string, nullable
    - `syntheticDisclosure` string: The show's synthetic-media line (legal gap G8a): it ends the feed's description and every episode's notes, is the feed's `podcast:txt` (purpose synthetic-media) and shows on the show's pages. Reword it; it can never be empty (default: "Voices and music in this show are synthetic.").
    - `metadata` unknown
    - `feedUrl` string, uri, nullable, required
    - `visibility` 'public' | 'unlisted' | 'private': What the show's slug exposes: public (landing page, archive, and the podcast feed), unlisted (shared episode links and the podcast feed, reached by link: never indexed, and the feed asks podcast directories not to list it), or private (nothing; the feed answers 404). A draft show's feed is served too.
    - `standingApproval` PublishingStandingApproval: ONE API key may approve this show's table-read plans (`POST /story-plans/{planId}/approve`) and publish its audio releases (`POST /publishing-releases/{releaseId}/publish`) without `userConfirmed`, for plans and releases created after `grantedAt`. Every other caller still needs `userConfirmed`. Not exercised while the series is paused or archived; never covers season runs.
      - `keyId` string, nullable, required: The bound API key (an outside app makes the show); null when Sleeper's show runner holds the grant (the show is on).
      - `keyName` string, nullable, required
      - `keyStart` string, nullable, required: The key's visible prefix, never the secret.
      - `grantedAt` string, date-time, required
      - `grantedBy` string, nullable, required
    - `destinations` PublishingDestination[]
      - `id` string, required
      - `seriesId` string, required
      - `projectId` string, required
      - `provider` 'rss' | 'apple_podcasts' | 'spotify' | 'youtube' | 'tiktok' | 'instagram' | 'linkedin' | 'x' | 'other', required
      - `medium` 'audio' | 'video' | 'short_form' | 'text' | 'mixed', required
      - `key` string, required
      - `label` string, nullable
      - `status` 'draft' | 'connected' | 'needs_auth' | 'needs_setup' | 'disconnected' | 'failed' | 'archived', required
      - `providerAccountId` string, nullable
      - `providerConfigId` string, nullable
      - `scopes` string[]
      - `settings` unknown
      - `externalUrl` string, uri, nullable
      - `lastVerifiedAt` string, date-time, nullable
      - `createdAt` string, date-time, required
      - `updatedAt` string, date-time, required
    - `createdAt` string, date-time, required
    - `updatedAt` string, date-time, required

## Other responses

- `400`: Request payload failed validation. The error message names the offending field.
- `401`: Missing, malformed, revoked, or expired API key.
- `403`: API key is valid but lacks the scope, is blocked by IP allowlist, or the account is disabled.
- `429`: Rate limit for this API key + IP + route family was exceeded.
- `500`: The API failed unexpectedly. Safe to retry with the same Idempotency-Key.

---

[API](https://skmtc.dev/sleeperhit/apis/sleeper-hit-studio-story-api.md) · [All operations](https://skmtc.dev/sleeperhit/apis/sleeper-hit-studio-story-api/llms.txt) · [OpenAPI document](https://skmtc.dev/sleeperhit/apis/sleeper-hit-studio-story-api/revisions/4534f5d25540?raw)
