---
title: Videos, shorts, and Voiceovers
description: Generate media, upload source files, check progress, and retrieve downloads through the API.
---

Use `https://api.jellypod.com/v1` as the base URL. Authenticate each API
request with `Authorization: Bearer YOUR_API_KEY`. Create a key in
[Developers](https://studio.jellypod.com/settings/api-keys).

Videos and shorts are one product: the requested length decides which you get,
and `/shorts` is an alias of `/videos` that defaults to 30 seconds. Either
collection reads the other's IDs. Voiceovers have their own `/voiceovers`
collection.

See [Source-to-media recipes](/api/source-to-media-recipes) for copyable upload, creation, status and download requests. These examples still need isolated end-to-end output verification.

## Discover settings before creating

Call `GET /videos/options`, `GET /shorts/options`, or
`GET /voiceovers/options` for each product's supported settings and default
values. These are fixed product fallbacks, not anything saved for your
organization; voiceover has no style selection.

Use these shared endpoints alongside product options:

| Endpoint | Use |
| --- | --- |
| `GET /account` | Check your organization, plan, and `data.credits.balance`. |
| `GET /hosts` | Find a narrator owned by your organization. |
| `GET /sources` | Find existing research or uploaded visual sources. |

Prompt and voiceover creation require a narrator: call `GET /hosts` to find
one, then send its ID in `host_id`, or up to four in `host_ids` for a video or
Voiceover. Finished scripts resolve their bracketed speaker labels instead.

## Create and retrieve media

| Action | Video | Short | Voiceover |
| --- | --- | --- | --- |
| Create | `POST /videos` | `POST /shorts` | `POST /voiceovers` |
| List | `GET /videos` | `GET /shorts` | `GET /voiceovers` |
| Retrieve | `GET /videos/{id}` | `GET /shorts/{id}` | `GET /voiceovers/{id}` |
| Progress | `GET /videos/{id}/status` | `GET /shorts/{id}/status` | `GET /voiceovers/{id}/status` |
| Retry | `POST /videos/{id}/retry` | `POST /shorts/{id}/retry` | `POST /voiceovers/{id}/retry` |
| Download | `GET /videos/{id}/download` | `GET /shorts/{id}/download` | `GET /voiceovers/{id}/download` |
| Delete | `DELETE /videos/{id}` | `DELETE /shorts/{id}` | `DELETE /voiceovers/{id}` |

Episodes serve the same progress, retry, and download endpoints under
`/episodes/{episode_id}`; see [Episode progress, retries, and downloads](/api/getting-started#episode-progress-retries-and-downloads).

Delete returns `204` with no body and permanently removes the item. A
voiceover ID returns `404` under `/videos` or `/shorts`, and a video or short ID
returns `404` under `/voiceovers`. `GET /videos` lists videos and shorts
together. `GET /shorts` is deprecated and returns the same list.

Generation uses the same [product credit rates as Studio](/getting-started/understanding-credits).
Status checks and downloads do not generate new content. Open the returned
`studio_url` to edit narration or individual scenes.

### Idempotency keys

Create and retry requests accept an optional `Idempotency-Key` header: 1 to 255
printable ASCII characters, without spaces. Without it, every request starts a
new generation, including an automatic retry of a request whose response was
lost. With it, resending the same request within 24 hours returns the original
resource instead of generating again. Reusing a key with different inputs
returns `409`. A deliberate retry of a failed generation uses the retry
endpoint and a new key.

The header applies to the product endpoints above and to
`POST /episodes/generate`. Other writes do not gain idempotency protection
from it.

### Create a Video

Send this JSON to `POST /videos`:

```json
{
  "prompt": "Explain how bees communicate, for a curious beginner.",
  "host_ids": ["host_id_1", "host_id_2"],
  "target_duration_seconds": 300,
  "language": "en",
  "orientation": "LANDSCAPE"
}
```

Durations are `30` or `60` seconds for a short, and `180`, `300`, `420`,
`600`, `900`, `1200`, or `1500` seconds for a video; the length you send picks
the product. Omit `target_duration_seconds` to default to `300` seconds, or,
with a `script`, to measure the script's spoken length. `host_ids` accepts one
to four unique host IDs in cast order. A video may send one `host_id` instead,
but a request cannot include both fields, and a narrator is required.

### Create a short

```json
{
  "prompt": "Explain how bees communicate, for a curious beginner.",
  "target_duration_seconds": 30,
  "background_music": true
}
```

`POST /shorts` takes the same request as `POST /videos` and defaults to `30`
seconds; `180` or more there makes a video. `background_music` applies to
shorts, is optional, and defaults to on.

### Create from a finished script

Videos and shorts accept `script` instead of `prompt`:

```json
{
  "script": "[Alex]\nWelcome to the show.\n\n[Sam]\nToday we are explaining how bees communicate.",
  "style_id": "your_style_id"
}
```

Put `[Character Name]` on its own line before each turn. Each label matches an
active character returned by `GET /hosts`, or assign Host IDs to labels
explicitly with `speaker_characters`:

```json
{
  "speaker_characters": [{ "label": "Alex", "character_id": "host_id_1" }]
}
```

Explicit assignments override name matching and must use active hosts; unmapped
labels still match by name. Script mode does not accept `host_id`, `host_ids`,
or `source_ids`. `style_id` is required. Jellypod validates and resolves the
complete script before starting Credit-bearing generation.

A label that matches no active character returns `422` with
`reason: "unresolved_speakers"` and the unmatched labels in `speakers`. Video
and short speakers can resolve to at most four distinct characters; several
labels may share one. A script past that returns `400` with
`code: "host_ceiling"`. Assign extra labels to a character already in the
script with `speaker_characters`, or remove speakers, then retry.

### Create a Voiceover

First upload a supported document or image. Send this JSON to
`POST /voiceovers`, replacing the example UUID with your source ID:

```json
{
  "source_ids": ["00000000-0000-4000-8000-000000000001"],
  "direction": "Walk through the slides in order for new team members.",
  "captions_enabled": true,
  "visual_motion": "auto"
}
```

`source_ids` contains 1 to 10 unique uploaded-file source IDs in presentation
order. The combined input can contain at most 50 visuals. Generation waits for
Source processing and prepares missing visuals.

Omit `target_duration_seconds` for automatic duration based on the content.
Explicit durations are `180`, `300`, `420`, `600`, `900`, `1200`, `1800`, `2700`,
or `3600` seconds, subject to your plan's voiceover limit. `direction` accepts up
to 4,000 characters. `captions_enabled` defaults to `false`. `visual_motion`
defaults to `auto`, which zooms photos and holds document pages still.
Send `dynamic` to zoom every visual or `static` to zoom none.

### Optional settings

All three products accept `language` and `orientation`. In prompt mode, shorts
accept one narrator in `host_id`; videos accept one to four narrators in
`host_ids` and retain `host_id` as a one-narrator compatibility input.
Voiceovers use the same narrator fields. `language` defaults to `en` when
omitted, and `orientation` defaults to `AUTO`.
Orientation accepts `AUTO`, `LANDSCAPE`, `PORTRAIT`, or `SQUARE`. Omitting
orientation, or sending `AUTO` explicitly, means portrait for shorts and
landscape for videos and voiceovers. Portrait and square are also supported
for voiceovers.

Videos and shorts require exactly one of a nonblank `prompt` or `script`.
Prompt mode always writes new narration using Sources as reference material.
Use `script` to preserve finished narration word for word.
Prompts accept up to 20,000 characters and scripts up to 75,000. Prompt mode
also accepts `style_id` from product options and up to 100 unique `source_ids`
for research. Style defaults to the product's default style when omitted, but
script mode requires it.
Processing sources can be attached immediately: the generation workflow waits
before reading them. Missing, failed, or stalled sources are rejected.
Complete the upload before attaching a newly reserved source.

`background_music` belongs to shorts. `captions_enabled`, `direction`, and
`visual_motion` belong to voiceovers, which do not accept `prompt` or
`style_id`.

## Follow progress and download

Creation returns HTTP `202` with the resource in `data`, a `Location` header
pointing to its status endpoint, and `Retry-After: 10`. Save `data.id`
immediately. The resource also includes `object`, `title`, `status`, `phase`,
`error`, `generation_id`, `studio_url`, `host_id`, `host_ids`, `language`,
`orientation`, `background_music`, `style_id`, `target_duration_seconds`,
`created_at`, and `updated_at`.

Poll the status endpoint. `status` is `idle`, `generating`, `completed`, or
`failed`. Use `phase` for progress detail and `error` for a failure reason. A
`completed` resource reports `phase` as `null`.
Respect `poll_after_seconds`; generation can continue after your script stops.

Videos and shorts automatically render after generation. While rendering,
status stays `generating` with phase `rendering_video`. A render failure reports
`failed`; call `/retry` to render the saved draft again without regenerating
content or spending generation credits.

An incomplete draft returns `idle` with phase `ready_to_render` and an
error explaining that the draft needs attention in Studio. Open `studio_url`
to finish it. Voiceovers remain editable drafts until rendered in Studio.

Once generation completes, request `/download`. Its `data` contains `id`,
`object`, and `delivery`:

- `{"kind":"available","url":"https://example.com/output.mp4","origin":"mux"}` contains the download URL. The origin may also be `storage`.
- `{"kind":"preparing","reason":"ingest"}` means the delivery file is still being prepared. Wait and check again.
- `{"kind":"unavailable"}` means no downloadable output is available. Open the resource in Studio.

List endpoints accept `limit` (1 to 100, default 20) and an opaque `cursor`.
They return `{"data": [...]}` with the newest items first. Follow the response's
`Link` header with `rel="next"` for the next page; no next link means the end.
Pagination metadata is in the HTTP header, not in the JSON body.

## Upload a file for Voiceover

Request an upload slot with `POST /sources/uploads`, upload the bytes to
`data.upload.url` with its method and headers, then call
`POST /sources/{id}/complete-upload`. Use the actual byte size and MIME type.
Upload slots expire at `data.upload.expires_at`. Send the API key only to
Jellypod API endpoints, not the signed upload URL.

Files must be nonempty and no larger than 100 MiB (104,857,600 bytes).
`file_name` must be a filename without directories, at most 255 characters;
its extension must match `mime_type`. Optional `title` accepts 1 to 500
characters. Completion verifies the declared size, stored MIME type, and
supported file signatures.

Voiceover supports these source types:

| File | MIME type |
| --- | --- |
| PDF | `application/pdf` |
| PowerPoint `.pptx` | `application/vnd.openxmlformats-officedocument.presentationml.presentation` |
| PNG | `image/png` |
| JPEG `.jpg` or `.jpeg` | `image/jpeg` |
| WebP | `image/webp` |

Other presentation formats (`.ppt`, `.pptm`, Keynote, OpenDocument) are not
accepted for Voiceover; export to PDF or `.pptx` first.

A voiceover supports 50 pages or images combined across all its sources.
Splitting a presentation into multiple files does not raise this total.

Other supported upload types can be used as research sources. API uploads do
not accept `video/*`; use extracted audio instead. Use `POST /sources` for a
URL, YouTube link, or inline text.

A source moves from `awaiting_upload` through `processing` to `completed` or
`error`. Check `GET /sources/{id}`. Request a new slot if it expires or
processing fails.

## Handle errors and retries

Read the error before retrying. Correct invalid input, add credits when needed,
and respect `Retry-After` for rate limits. A failed HTTP response does not prove
that generation failed.

Errors can include a `recovery` object: an `action` such as `correct_input`,
`check_account`, or `retry`, and, on a pre-execution rejection, an
`idempotency_key` of `reuse` or `new`. This field is present even when the
request sent no `Idempotency-Key`; follow it only when you sent one. A
rejection proven to happen before generation started releases its key, so fix
the input or account issue and resend with the same key when
`idempotency_key` is `reuse`; `new` means start over with a different key.

If an idempotency conflict says acceptance is unresolved, check the resource ID
in the error. Do not create again with a new key because the original request
may be running. Completed requests replay their accepted response.

A resource with `status: "failed"` or an unstarted `status: "idle"` can be
retried with `POST /{collection}/{id}/retry`. Send a new `Idempotency-Key` if
the retry itself might be resent. The retry keeps the resource ID and starts a
new generation attempt. Review the error and credits first.
