New: 13 caption styles
Jellypod Docs

Source-to-media API recipes

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. API key access requires Creator or higher. 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.

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.

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:

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.

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.

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".

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:

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:

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.
  • 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 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.

Was this page helpful?