---
title: Source-to-media API recipes
description: Upload a source, request a Voiceover or illustrated Video, check progress, and retrieve the generated file.
---

These examples follow the current public API contracts. They have been checked
against the request schemas and existing automated tests; an isolated
upload-to-playback run has not yet been recorded. They are reference examples,
not a claim of verified output quality or editor parity.

Use a PDF or PowerPoint source for a Voiceover that narrates its prepared
pages. Use the same source as research for an illustrated Video that
generates new visuals. Review the narration and output before sharing. For
PowerPoint, use PDF if you need control over how each slide renders; do not
assume editable objects, animations, or transitions survive page preparation.

## Before you start

You need `curl`, `jq`, a supported local file, and an organization API key from
[Developers](https://studio.jellypod.com/settings/api-keys). API keys come
with every paid plan. Set `JELLYPOD_API_KEY` in your shell environment;
keep it out of request files, source control, and logs. Run these examples in a
private working directory with a synthetic source before using customer data.

```bash
set -euo pipefail
umask 077
: "${JELLYPOD_API_KEY:?Set your organization API key in the environment}"
API_BASE='https://api.jellypod.com/v1'

curl --fail-with-body --silent --show-error --proto '=https' \
  -H "Authorization: Bearer $JELLYPOD_API_KEY" \
  "$API_BASE/account" > account.json

curl --fail-with-body --silent --show-error --proto '=https' \
  -H "Authorization: Bearer $JELLYPOD_API_KEY" \
  "$API_BASE/hosts?limit=100" > hosts.json

curl --fail-with-body --silent --show-error --proto '=https' \
  -H "Authorization: Bearer $JELLYPOD_API_KEY" \
  "$API_BASE/voiceovers/options" > voiceover-options.json

curl --fail-with-body --silent --show-error --proto '=https' \
  -H "Authorization: Bearer $JELLYPOD_API_KEY" \
  "$API_BASE/videos/options" > video-options.json
```

Check `account.json` for the organization, plan and `data.credits.balance`.
Select a narrator from `hosts.json` and set `HOST_ID` to its ID. Follow the
response's `Link` header with `rel="next"` if you need more hosts. The API key's
organization owns the hosts and sources; an ID from another organization will
not work. The bearer key is organization access, not a learner login. These
endpoints do not require a separate product-specific OAuth scope.

## Upload one source

This example uses `training.pdf`. The MIME type must match the actual file.
Only upload material you have permission to process. The upload accepts a
local file, so this recipe does not ask Jellypod to fetch an arbitrary URL.

```bash
SOURCE_FILE='training.pdf'
test -f "$SOURCE_FILE"
SOURCE_BYTES=$(wc -c < "$SOURCE_FILE" | tr -d '[:space:]')

jq -n --arg name "$SOURCE_FILE" --argjson size "$SOURCE_BYTES" \
  '{file_name: $name, mime_type: "application/pdf", size_bytes: $size}' \
  > upload-request.json

curl --fail-with-body --silent --show-error --proto '=https' \
  -H "Authorization: Bearer $JELLYPOD_API_KEY" \
  -H 'Content-Type: application/json' \
  --data-binary @upload-request.json \
  "$API_BASE/sources/uploads" > upload-slot.json

SOURCE_ID=$(jq -er '.data.source.id' upload-slot.json)
UPLOAD_URL=$(jq -er '.data.upload.url' upload-slot.json)
UPLOAD_METHOD=$(jq -er '.data.upload.method' upload-slot.json)
UPLOAD_CONTENT_TYPE=$(jq -er '.data.upload.headers["content-type"]' upload-slot.json)

curl --fail-with-body --silent --show-error --proto '=https' \
  --request "$UPLOAD_METHOD" \
  -H "Content-Type: $UPLOAD_CONTENT_TYPE" \
  --data-binary "@$SOURCE_FILE" "$UPLOAD_URL"

curl --fail-with-body --silent --show-error --proto '=https' \
  --request POST -H "Authorization: Bearer $JELLYPOD_API_KEY" \
  "$API_BASE/sources/$SOURCE_ID/complete-upload" > source-completion.json
```

The signed upload URL and headers come from `upload-slot.json`. Send the API
key only to Jellypod's API, never to the signed upload or download URL. Keep the
slot response private and use it before `data.upload.expires_at`. Do not add
`--location` to authenticated API requests or upload requests.

Uploads are nonempty and at most 100 MiB (104,857,600 bytes). The filename must
not contain directory separators and must match the MIME type. Voiceover
visual sources support PDF, `.pptx`, PNG, JPEG and WebP. For PowerPoint, use
`application/vnd.openxmlformats-officedocument.presentationml.presentation`.
Use 1 to 10 unique uploaded sources in presentation order, with at most 50
pages/images combined. Legacy `.ppt`, `.pptm`, Keynote and OpenDocument are not
accepted as Voiceover visuals. File completion verifies the stored size, MIME
type and supported signature.

Poll the source before creating media:

```bash
curl --fail-with-body --silent --show-error --proto '=https' \
  -H "Authorization: Bearer $JELLYPOD_API_KEY" \
  "$API_BASE/sources/$SOURCE_ID" > source-status.json
jq -r '.data.status' source-status.json
```

Continue when `data.status` is `completed`. `awaiting_upload` means the upload
still needs completion; `processing` means wait and check again. `error` is a
source failure: inspect the response and correct the source. Upload reservation
and completion do not gain idempotency from a generation key. Save `SOURCE_ID`
and reuse the existing source rather than blindly reserving another upload.

## Recipe 1: Voiceover from the prepared pages

Set `HOST_ID` to an organization-owned narrator from `GET /hosts`. Choose a
new logical request key once, then preserve that key and the exact JSON if the
HTTP response is lost. Replace the example key for each intentional new job.

```bash
HOST_ID='YOUR_ORGANIZATION_HOST_ID'
VOICEOVER_KEY='training-voiceover-request-001'

jq -n --arg source "$SOURCE_ID" --arg host "$HOST_ID" \
  '{source_ids: [$source], host_id: $host,
    direction: "Narrate the approved training pages in order. Preserve the stated steps and caveats.",
    language: "en", orientation: "LANDSCAPE",
    captions_enabled: true, visual_motion: "static"}' \
  > voiceover-request.json

curl --fail-with-body --silent --show-error --proto '=https' \
  -D voiceover-response-headers.txt \
  -H "Authorization: Bearer $JELLYPOD_API_KEY" \
  -H "Idempotency-Key: $VOICEOVER_KEY" \
  -H 'Content-Type: application/json' \
  --data-binary @voiceover-request.json \
  "$API_BASE/voiceovers" > voiceover-response.json

VOICEOVER_ID=$(jq -er '.data.id' voiceover-response.json)
```

`visual_motion: "static"` holds the prepared visuals still. `auto` holds
document pages still and zooms image sources; `dynamic` zooms every visual.
Voiceover does not accept `prompt` or `style_id`. This request omits duration
so generation chooses it from the source content, subject to the plan limit.
Explicit supported durations come from `/voiceovers/options`. Narration is
generated from the source and direction; review it for accuracy.

## Recipe 2: Illustrated Video from the source

Reuse the completed `SOURCE_ID` and your narrator. Here the source is research
for a generated story and visuals, rather than a request to retain the slides.
The example uses the default Video style; choose a supported `style_id` from
`GET /videos/options` if you want to select one explicitly.

```bash
VIDEO_KEY='training-illustrated-video-request-001'

jq -n --arg source "$SOURCE_ID" --arg host "$HOST_ID" \
  '{source_ids: [$source], host_id: $host,
    prompt: "Explain this approved training source to new team members with illustrated scenes. Include its required steps and caveats; do not invent policy details.",
    language: "en", orientation: "LANDSCAPE",
    target_duration_seconds: 300}' > video-request.json

curl --fail-with-body --silent --show-error --proto '=https' \
  -D video-response-headers.txt \
  -H "Authorization: Bearer $JELLYPOD_API_KEY" \
  -H "Idempotency-Key: $VIDEO_KEY" \
  -H 'Content-Type: application/json' \
  --data-binary @video-request.json \
  "$API_BASE/videos" > video-response.json

VIDEO_ID=$(jq -er '.data.id' video-response.json)
```

Video prompt mode accepts up to 100 unique research source IDs and a prompt of
at most 20,000 characters. Both recipes can use one `host_id` or 1 to 4 unique
`host_ids`, but not both. Finished-script Video creation is a separate mode:
it requires `style_id`, resolves bracketed speaker labels, and rejects
`host_id`, `host_ids` and `source_ids`. These recipes do not export an edited
timeline, custom scene timing, or the Studio's complete editor state.

## Poll and download either result

Creation returns `202`, `data.id`, a `Location` status path, and
`Retry-After: 10`. Keep the ID even if you stop polling. Use the following for
Voiceover; for Video, set `COLLECTION='videos'` and `PRODUCT_ID="$VIDEO_ID"`.

```bash
COLLECTION='voiceovers'
PRODUCT_ID="$VOICEOVER_ID"

curl --fail-with-body --silent --show-error --proto '=https' \
  -H "Authorization: Bearer $JELLYPOD_API_KEY" \
  "$API_BASE/$COLLECTION/$PRODUCT_ID/status" > media-status.json
jq '{status: .data.status, phase: .data.phase,
     error: .data.error, poll_after_seconds: .data.poll_after_seconds}' \
  media-status.json
```

For `generating`, wait at least `poll_after_seconds` and poll again. Honor any
HTTP `Retry-After` too. Set a local observation deadline rather than poll
forever: stopping your script does not cancel generation. `failed` is terminal
for that attempt; inspect `error` before deciding whether to retry. `idle`
can indicate an unstarted resource. For `completed`, request its delivery:

```bash
curl --fail-with-body --silent --show-error --proto '=https' \
  -H "Authorization: Bearer $JELLYPOD_API_KEY" \
  "$API_BASE/$COLLECTION/$PRODUCT_ID/download" > delivery.json
jq '.data.delivery' delivery.json
```

`preparing` means the rendition or ingest is still pending: wait and check
download again. `unavailable` means there is no downloadable output; inspect
the resource's `studio_url`. Download only when the kind is `available`:

```bash
test "$(jq -r '.data.delivery.kind' delivery.json)" = 'available'
DOWNLOAD_URL=$(jq -er '.data.delivery.url' delivery.json)
curl --fail --silent --show-error --location \
  --proto '=https' --proto-redir '=https' \
  "$DOWNLOAD_URL" --output "$COLLECTION-output.mp4"
```

Play the downloaded output, compare the Voiceover's pages with the original,
and review narration, captions and illustrated scenes. Keep download URLs
private; retrieve a fresh delivery response if a URL stops working.

## Reruns, errors and costs

- Generation create/retry keys accept 1 to 255 printable ASCII characters
  without spaces. Within 24 hours, replay the same create JSON with the same
  key after a lost or uncertain response. Changing inputs with that key
  returns `409`. After the protection expires, resending can create and charge
  a new job, so retrieve the saved product ID first.
- On an unresolved-acceptance `409`, check the resource ID in the error.
  Do not switch to a fresh create key while the original may be running.
  If a rejection includes `recovery.idempotency_key`, follow `reuse` or `new`
  only for a request that actually sent a key.
- Correct invalid input, ownership, unsupported media, authentication or
  subscription errors before retrying. For `429`, wait for `Retry-After`.
  For a transient network/server error, reconcile the resource or replay the
  identical keyed request. Never automatically repeat an unkeyed write.
- To deliberately retry a `failed` or unstarted `idle` product, send
  `POST /voiceovers/{id}/retry` or `POST /videos/{id}/retry` with a new
  `Idempotency-Key`. The resource ID stays the same, but a new generation
  attempt starts. Check the error and balance first. There is no public
  cancellation or product deletion endpoint in these collections.
- Current generation rates are 30 credits per narration minute, 7 per
  generated image and 20 per generated video clip. Voiceover uses its uploaded
  visuals and incurs no generated-visual credits. Estimates reserve credits;
  actual generated usage settles the hold, including usage from failed runs.
  Status and download calls do not generate content. See
  [credit costs and holds](/getting-started/understanding-credits).
- Duration and access limits depend on the plan. Read product options and the
  account response, and inspect admission errors rather than assume every
  preset is available. General API rate limits are organization-wide and
  reported in `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`.
  [The product guide](/api/products) describes the response contracts.

After verification, remove local request, response and media files according
to your data policy. Unused sources can be deleted with
`DELETE /sources/{source_id}`; do not remove a source still needed for ongoing
generation or future edits. Manage generated products in Studio.
