---
title: Get started with the API
description: Programmatic access to the Jellypod platform.
---

The Jellypod API creates and manages hosts, sources, podcast episodes, videos,
shorts, and voiceovers. A host in the API is a saved Character in Studio.
Start with the [Video, short, and Voiceover guide](/api/products) for
creation, file uploads, progress, retries, and downloads.

For copyable request examples, see [Source-to-media recipes](/api/source-to-media-recipes).

## Connect

API requests need a paid plan or the free trial. Create an organization API key
in [Developers](https://studio.jellypod.com/settings/api-keys). The API Keys
tab is visible to admins and to members with permission to manage keys. Send
requests to `https://api.jellypod.com/v1` with this header:

```
Authorization: Bearer YOUR_API_KEY
```

## Credits and usage

The API uses the same product credit rates and plan limits as Studio.
Generation costs depend on the product and its settings. Check `GET /account`
for your plan and available balance. Podcast rendering and publishing are free;
status checks and downloads do not start new generation. See
[Understanding Credits](/getting-started/understanding-credits)
for details.

## Podcast generation and retries

For `POST /episodes/generate` and `POST /episodes/import`, send an optional
`Idempotency-Key` header. Reuse the key only with the same request. For 24
hours, Jellypod then returns the original episode and workflow IDs without
starting another generation. Reusing the key with different input returns
`409`. Without a key, every request starts a generation.

If acceptance is uncertain, check the episode ID in the error before creating
another episode. Video, short, and voiceover
[create and retry endpoints](/api/products) accept the same optional key.

## Choose episode outputs

Send `outputs` with `POST /episodes/generate` or `POST /episodes/import` to
choose the files you need:

| `outputs` | Result |
| --- | --- |
| `["audio"]` | MP3, without video work |
| `["video"]` | MP4 with sound |
| `["audio", "video"]` | Both files, available independently |
| `[]` | Editable content and narration, without exported files |

For example, add `"outputs": ["audio"]` to your existing generation request
to get an MP3 without waiting for video. Poll `GET /episodes/{episode_id}`.
Its `outputs.audio` and `outputs.video` each contain `status`, `download_url`,
`current`, and `error`. A status of `available` supplies a download URL;
`preparing` means the requested file is still being produced; `failed` reports
an error; `not_requested` means no file of that format was requested.
Audio can be available while video is still preparing. A video failure does
not remove a successful audio file.

The episode's `status` still describes publication (`draft`, `published`, or
`scheduled`). Read `progress.status` for production: `completed` means
narration and all requested files finished. With `outputs: []`, it means the
editable content is ready. A completed request can still have content gaps,
reported in `progress.needs_attention`.

Existing requests keep their behavior: `render: true` produces audio and video;
`render: false` or omission of both fields leaves an editable draft. `render`
is deprecated. Send either `outputs` or `render`; sending both is rejected.
Generation uses credits; exporting saved content does not regenerate narration.

## Export saved content

After editing in Studio, or generating with `outputs: []`, request files with:

```http
POST /v1/episodes/{episode_id}/export
Content-Type: application/json
Idempotency-Key: your-unique-export-key

{"outputs": ["audio"]}
```

The export action accepts audio, video, or both and returns `202` with `id`,
`outputs`, and `progress`. Its `Location` header points to the episode. Poll
that URL at the returned interval. Export reuses current files when possible,
produces missing or outdated files from saved content, and never publishes the
episode. Publishing remains an explicit action.

A failed replacement preserves the previous successful download. `current`
indicates whether the available file reflects the episode's current content.
Request export again with a new idempotency key to retry failed output work;
use the same key only when resending an identical request after an uncertain
response. The existing generation retry endpoint has different behavior and
can charge generation credits again.

Download links are signed and expire. Storage links last 24 hours; fetch the
episode or its download endpoint again for a fresh link. Store the episode ID
rather than treating a signed URL as a permanent identifier.

## Episode progress, retries, and downloads

Episodes share the status, retry, and download endpoints that videos, shorts,
and voiceovers use. `GET /episodes/{episode_id}` still returns the full episode
with its embedded `progress`.

| Action | Endpoint |
| --- | --- |
| Progress | `GET /episodes/{episode_id}/status` |
| Retry | `POST /episodes/{episode_id}/retry` |
| Download | `GET /episodes/{episode_id}/download` |
| Export saved content | `POST /episodes/{episode_id}/export` |
| Captions and watermark | `GET` or `PATCH /episodes/{episode_id}/video-settings` |

The video settings endpoints (also under `/videos`, `/shorts`, and
`/voiceovers`) read and change caption visibility, caption style, and the
watermark corner the next render uses. A value saved on the item overrides the
podcast default for an episode and the organization default for a video, and
`null` returns that field to its default. Upload new watermark images in Studio.

The status endpoint returns `id`, `object` (`episode`), `status`, `phase`,
`error`, `needs_attention`, `poll_after_seconds`, and `studio_url`. While
`poll_after_seconds` is above zero, the `Retry-After` header carries the same
wait. `failed` means requested work could not finish; successful files remain
available. A usable episode with gaps reports `needs_attention` as counts of
`unvoiced_lines`, `missing_visuals`, and `skipped_sources`; fix them in Studio.
It is `null` when nothing is missing.

Retry restarts an episode whose first generation failed, from the request that
started it, and charges generation credits again. It returns `202` with the
episode in `data`, a `Location` header pointing to the status endpoint, and
`Retry-After: 10`. It accepts an optional `Idempotency-Key`. A `409` means
nothing started: the episode is still generating, already has playable audio,
or failed in a way another attempt cannot fix, and the error says what to do.
Retry failed file production with `POST /episodes/{episode_id}/export`; this
generation retry endpoint does not retry export work.

Download returns `id`, `object`, `media`, and `delivery` for the served render.
Published and scheduled episodes keep serving that version until publication
changes. Use the episode's `outputs` download URLs to retrieve newer draft
exports. Draft episodes can download each requested file as it finishes.
Send `media=video` for the video file; the default is `audio`. `delivery` has
the same `available`, `preparing`, and `unavailable` forms as
[product downloads](/api/products#follow-progress-and-download).

## Import script text

`POST /episodes/import` accepts script or transcript text. It maps bracketed
speaker labels to hosts that already exist in your organization, or assigns
Host IDs to labels explicitly with `speaker_characters`, and queues a new
episode. A label matching no host, with no explicit assignment, is refused,
rather than creating a Host for it: a `422` with `reason: "unresolved_speakers"`
and a `speakers` array naming the unmatched labels. Create any host you plan to
name first, with `POST /hosts` or in Studio, then use its exact name as the
label or its ID in `speaker_characters`.

```json
{
  "podcast_id": "podcast_id",
  "script": "[Host One]\nWelcome to the show. Today we're talking about how small daily habits can make a big difference.\n\n[Host Two]\nI love that topic. Let's start with the easiest habit someone can try this week."
}
```

Text import removes clearly unspoken titles, production rules, and visual or
duration directions while preserving narration and inline delivery tags.
Uncertain lines stay; if classification fails, text is imported as written and
production notes may be narrated. Structured scripts keep the supplied text.

Imported text can contain up to 75,000 characters. Text longer than your plan's
maximum episode length is cut to that length. Content rejected by the
speech provider also returns `422` and creates nothing. The episode stays editable without rendering by
default. Set `outputs: ["audio"]` when you want an MP3, or request video
or both formats. Poll
`GET /episodes/{episode_id}` for progress and download URLs.

An unknown `podcast_id`, a speaker label matching no existing host, or text
with no usable segments also return an error and create nothing. Sent with an
`Idempotency-Key`, these input errors are rejected immediately, so fix the
input and retry with the same key when `recovery.idempotency_key` is `reuse`.
If cleanup failed or the rejection was stored under the older policy, recovery
instead specifies `new`.

## Generate from a prompt

Send `prompt` to `POST /episodes/generate`. You can include a duration in the
prompt. Without one, the target is about 7 minutes.

You can also set the duration directly in `options` instead of relying on the
prompt:

```json
{
  "podcast_id": "podcast_id",
  "prompt": "An episode about the history of beekeeping.",
  "options": {
    "episode_length_minutes": 15
  }
}
```

`episode_length_minutes` accepts an integer from 1 to 75 and defaults to 7. The
retired `episode_length` and `web_search` options are still accepted but have no
effect.

Episode prompt mode always writes a new script from the prompt and attached
sources, even when they contain finished narration. To preserve raw script text,
use `POST /episodes/import` with optional `speaker_characters` assignments.

## Generate from a script

Send a structured `script` to `POST /episodes/generate` to use your own chapters
and segments.

Use `GET /hosts` to fetch the `host_id` values available to your organization.
Every segment must reference one of those hosts, and each host must be active.
Imported script text is capped at 75,000 total characters across all segments.

```json
{
  "podcast_id": "podcast_id",
  "script": {
    "chapters": [
      {
        "title": "Introduction",
        "segments": [
          {
            "host_id": "host_id_1",
            "text": "Welcome to the show. Today we're talking about how small daily habits can make a big difference."
          },
          {
            "host_id": "host_id_2",
            "text": "I love that topic. Let's start with the easiest habit someone can try this week."
          }
        ]
      }
    ]
  }
}
```

Rules for script generation:

- Send either `prompt` or `script`, not both.
- `source_ids` are only supported for prompt-based generation.
- Script mode does not use `web_search`, `episode_length`, or
  `episode_length_minutes`.
- Scripts can include up to 50 chapters, 200 segments per chapter, and 600
  segments total. Each segment can contain up to 5,000 characters.
- Long scripts require enough available credits before generation starts. The
  final charge is based on the narration audio generated, whether or not the
  episode is rendered.
- Script generation creates a new episode.
