Videos, Shorts, and Voiceovers
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.
Videos, Shorts, and Voiceovers have separate collections, matching Studio.
A Short appears under /shorts; a Voiceover appears under /voiceovers.
Every operation checks that the resource belongs to your organization.
Discover settings before creating
Call GET /videos/options, GET /shorts/options, or
GET /voiceovers/options for the product you want to create. Each response has a
data object with supported_duration_seconds, styles (each with id, name,
and description), and your organization's default_host_id,
default_language, default_orientation, default_style_id, and
default_duration_seconds. Voiceover has no style selection, so its styles
array is empty and its default style and duration are null.
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 /voices | Browse voices when creating or updating a Host. |
GET /sources | Find existing research or uploaded visual Sources. |
If default_host_id is null, choose a Host and include its ID in host_id,
or save a default narrator in Studio. Product options describe available
settings; creation still checks your plan limits and credits.
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 |
Generation uses the same product credit rates as Studio.
Status checks and downloads do not generate new content. Open the returned
studio_url to edit narration or individual scenes.
Save an idempotency key
Create and retry requests for these three products require an Idempotency-Key
header: 1 to 255 printable ASCII characters, without spaces. Save a unique key
before sending the request. If the response is lost, resend the same request
with that key. Reusing it with different inputs returns 409. A deliberate
retry of a failed generation uses the retry endpoint and a new key.
This requirement applies to the product endpoints above. For Podcasts,
POST /episodes/generate accepts an optional key; other writes do not gain
idempotency protection from this header.
Create a Video
Send this JSON to POST /videos with your API key, Content-Type: application/json, and a saved Idempotency-Key:
{
"prompt": "Explain how bees communicate, for a curious beginner.",
"target_duration_seconds": 300,
"language": "en",
"orientation": "LANDSCAPE"
}Video durations are 300, 420, 600, 900, or 1200 seconds. Omit
target_duration_seconds to use a supported saved duration, or 420 seconds
if no supported duration is saved.
Create a Short
curl --fail-with-body --max-time 30 \
'https://api.jellypod.com/v1/shorts' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: YOUR_SAVED_UNIQUE_REQUEST_KEY' \
--data '{
"prompt": "Explain how bees communicate, for a curious beginner.",
"target_duration_seconds": 30,
"background_music": true
}'Shorts support 30 or 60 seconds and default to 30. background_music
is optional and uses your saved setting when omitted.
Create a Voiceover
First upload and process a supported document or image with
prepare_visuals: true, as shown below. Send this JSON to POST /voiceovers
with your API key, JSON content type, and a saved Idempotency-Key. Replace the
example UUID with your completed Source ID:
{
"source_ids": ["00000000-0000-4000-8000-000000000001"],
"direction": "Walk through the slides in order for new team members.",
"captions_enabled": true,
"visual_motion": "static"
}source_ids contains 1 to 10 unique uploaded-file Source IDs in presentation order. The
combined input can contain at most 50 visuals. Voiceover requires completed
Sources with prepared visuals; a completed text extraction alone is not enough.
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
accepts static (the default) or dynamic.
Optional settings
All three products accept host_id, language, and orientation. The narrator
and language use your saved organization settings when omitted. Orientation
accepts AUTO, LANDSCAPE, PORTRAIT, or SQUARE; omitted or AUTO means
portrait for Shorts and landscape for Videos and Voiceovers. An explicit
portrait or square orientation is also supported for Voiceovers.
Videos and Shorts require a nonblank prompt of up to 20,000 characters. They
also accept style_id from product options and up to 100 unique source_ids
for research. Style defaults to your saved setting. 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.
Request bodies reject unknown fields. background_music belongs to Shorts;
captions_enabled, direction, and visual_motion belong to Voiceovers.
Voiceover uses uploaded visuals and does 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, style_id,
target_duration_seconds, created_at, and updated_at.
Poll the status endpoint, which returns this shape:
{
"data": {
"id": "00000000-0000-4000-8000-000000000001",
"object": "short",
"status": "generating",
"phase": null,
"error": null,
"generation_id": null,
"studio_url": "https://studio.jellypod.com/shorts/00000000-0000-4000-8000-000000000001",
"poll_after_seconds": 10
}
}status is idle, generating, completed, or failed. Use phase for
progress detail when present and error for a failure reason. Respect
poll_after_seconds and stop polling at a reasonable time limit; generation can
continue after your script stops. completed and failed are terminal states
for the current attempt.
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 bestorage.{"kind":"preparing","reason":"rendition"}means the delivery file is still being prepared. The reason may also beingest. 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.
Developers includes JavaScript, Python,
and cURL examples with bounded status and download polling for each product.
Python examples require requests; shell examples require Bash, cURL, and jq.
The JavaScript examples run in Node.js with built-in fetch.
Upload a file for Voiceover
Request an upload slot with POST /sources/uploads, upload the file bytes to
data.upload.url with the returned method and headers, then call
POST /sources/{id}/complete-upload. Set prepare_visuals: true when requesting
PDF, presentation, or image sources for Voiceover. Use the file's actual byte
size and matching MIME type. Upload slots expire after two hours, at
data.upload.expires_at; finish both the byte upload and completion request
before that time.
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.
With prepare_visuals: true, supported types are:
| File | MIME type |
|---|---|
application/pdf | |
PowerPoint .ppt | application/vnd.ms-powerpoint |
PowerPoint .pptx | application/vnd.openxmlformats-officedocument.presentationml.presentation |
PowerPoint .pptm | application/vnd.ms-powerpoint.presentation.macroEnabled.12 |
Keynote .key | application/vnd.apple.keynote |
OpenDocument .odp | application/vnd.oasis.opendocument.presentation |
| PNG | image/png |
JPEG .jpg or .jpeg | image/jpeg |
| WebP | image/webp |
Each visual Source is limited to 50 pages or images. Larger presentations must be split into separate generation requests to respect the combined 50-visual Voiceover limit.
prepare_visuals defaults to false. General research uploads also accept
supported text, document, spreadsheet, image, and audio types, such as
text/plain (.txt), text/markdown (.md), text/csv (.csv),
application/vnd.openxmlformats-officedocument.wordprocessingml.document
(.docx), and audio/mpeg (.mp3). API uploads do not accept video/* MIME
types; upload extracted audio for video research. Use POST /sources for a URL,
YouTube link, or inline text instead of a file slot.
A reserved Source starts at awaiting_upload. After completion it moves through
processing to completed or error. Check GET /sources/{id}. Calling
complete-upload again for an already processing or completed Source returns
its current record. If the slot expires or processing fails, request a new
slot. A 409 during completion means another completion is in progress; retrieve
the Source before retrying.
If your client cannot transfer local files, open Studio's
Assets library and choose New Asset.
Supported documents and images uploaded there are prepared for Voiceover.
Use GET /sources to find the Source ID, then wait for completion before
creating a Voiceover.
This Node.js example uploads a PDF and waits for the Source to finish processing. It prints the Source ID to use in the Voiceover quickstart.
import { readFile } from 'node:fs/promises';
const base = 'https://api.jellypod.com/v1';
const authorization = 'Bearer YOUR_API_KEY';
async function api(path, options = {}) {
const response = await fetch(base + path, {
...options,
headers: { Authorization: authorization, ...options.headers },
signal: AbortSignal.timeout(30_000),
});
const result = await response.json();
if (!response.ok) throw new Error(JSON.stringify(result));
return result.data;
}
const bytes = await readFile('./presentation.pdf');
const slot = await api('/sources/uploads', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
file_name: 'presentation.pdf',
mime_type: 'application/pdf',
size_bytes: bytes.byteLength,
prepare_visuals: true,
}),
});
console.log('Save this Source ID:', slot.source.id);
const upload = await fetch(slot.upload.url, {
method: slot.upload.method,
headers: slot.upload.headers,
body: bytes,
signal: AbortSignal.timeout(120_000),
});
if (!upload.ok) throw new Error('File upload failed: ' + upload.status);
await api('/sources/' + slot.source.id + '/complete-upload', { method: 'POST' });
for (let attempt = 0; attempt < 120; attempt++) {
const source = await api('/sources/' + slot.source.id);
if (source.status === 'completed') {
console.log('YOUR_PROCESSED_SOURCE_ID:', source.id);
break;
}
if (source.status === 'error') throw new Error('Source processing failed. Request a new upload slot.');
if (attempt === 119) throw new Error('Polling stopped. Check the saved Source ID before uploading again.');
await new Promise(resolve => setTimeout(resolve, 10_000));
}Send the Jellypod API key only to Jellypod API endpoints. The file transfer uses the signed upload URL and headers returned by the API.
Handle errors and retries
Read the error response before retrying. Invalid input needs correction;
insufficient credits require an available balance. A recorded rejection before
dispatch replays the same error; after correcting it, use a new key for the new
request. For rate limits, respect
Retry-After. A failed HTTP response does not prove that generation failed.
If an idempotency conflict says acceptance is unresolved, check the resource ID
in the error. Do not create again with a new key: the original dispatch may
already be running. Keep the command ID for support if the resource cannot be
found. Completed requests replay their accepted response; fetch /status for
current progress.
A resource with status: "failed" or an unstarted status: "idle" can be retried with
POST /{collection}/{id}/retry and a new Idempotency-Key. The retry keeps the
resource ID and starts a new generation attempt. Review the error and credits
before starting that attempt.
Was this page helpful?