Videos
Jobs & polling
Video endpoints return a workflow_id, and this endpoint is how you follow it: status, cost as it settles, and the finished video URL. No webhooks — poll it.
The status endpoint
Free to call, as often as you like. The response is the job's whole story:
| Field | Type | Description |
|---|---|---|
| workflow_id | string | The id you got when queueing. |
| status | enum | queued → running → completed or failed. |
| video_url / image_url | string | null | The finished output, present once completed. |
| result | object | null | video_url / image_url / audio_url / asset_id when present, plus poster_url, editable (can be edited) and scenes_url (its GET /videos/…/scenes link). |
| credits_estimated | int | null | The reservation made at queue time. |
| credits_actual | int | null | The measured charge, recorded at completion. |
| credits_consumed | int | null | The one to display: null while queued/running, the actual charge once completed (never more than reserved), 0 on failed (refunded). |
| job_type | string | See job types. |
| phase | enum | queued → preparing → generating → assembling → finalizing → completed | failed. |
| progress | int 0–100 | null | Rough percentage for a progress bar. |
| poster_url | string | null | A still of the video, once available. |
| source_workflow_id | string | null | On edits: the video that was edited. |
| error_code | string | null | Machine-readable reason a job failed — below. |
| error_message | string | null | A fixed, user-safe explanation you can show. |
| queued_at / completed_at | timestamps | For your own duration metrics. |
{
"workflow_id": "partner_h3v_5d0e9b2c4f1a4e7b9c3d2a1f0e9d8c7b",
"status": "completed",
"job_type": "multi_scene_video",
"phase": "completed",
"progress": 100,
"error_code": null,
"error_message": null,
"credits_estimated": 131,
"credits_actual": 131,
"credits_consumed": 131,
"video_url": "https://…/video.mp4",
"poster_url": "https://…/poster.jpg",
"result": {
"video_url": "https://…/video.mp4",
"asset_id": "…",
"poster_url": "https://…/poster.jpg",
"editable": true,
"scenes_url": "…/api/partner/v1/videos/partner_h3v_5d0e…/scenes"
},
"queued_at": "2026-07-08T04:12:23.595Z",
"completed_at": "2026-07-08T04:31:02.114Z"
}A sane poll loop
- Interval: every 8–10 seconds is plenty (30 s after 10 minutes) — jobs take minutes, not milliseconds.
- Timeout: give talking-head jobs ~10 minutes and scene videos ~25 before alarming. A job that stays quiet for 75 minutes is failed for you with
STALE_TIMEOUTand refunded. - Terminal states are terminal. Once
completedorfailed, the record never changes — cache it and stop polling.
async function waitForJob(workflowId) {
for (;;) {
const res = await fetch(
`https://platform.promoat.ai/api/partner/v1/jobs/${workflowId}/status`,
{ headers: { Authorization: `Bearer ${process.env.PROMOAT_KEY}` } },
);
const job = await res.json();
if (job.status === 'completed') return job;
if (job.status === 'failed') throw new Error(job.error_message ?? 'job failed');
await new Promise((r) => setTimeout(r, 8000));
}
}Showing progress to your users
Show phase as a label and progress as a bar; once poster_url appears you can show a thumbnail. The same metadata.phase is on each row of GET /usage/jobs.
Warnings raised while the job runs — for example SCENE_PHOTO_DROPPEDwhen a scene photo couldn't be used — are on the job's GET /usage/jobs row as metadata.warnings (all warning codes). Warnings about the request itself come back right away, on the POST /videos response.
Job types and workflow ids
| job_type | workflow_id prefix | Made by |
|---|---|---|
talking_head_video | partner_h3t_ | POST /videos, talking head |
multi_scene_video | partner_h3v_ | POST /videos, scenes |
scene_edit_video | partner_se_ | fix-scene, edit-script |
fix_sound_video | partner_fs_ | fix-sound |
New ids are the prefix plus 32 hex characters. Jobs made before 1.1.0 keep their old ids and prefixes.
Why a job failed
| error_code | Meaning |
|---|---|
PROVIDER_MODERATION | The content was refused by a safety check. Change the script, idea or photos. |
PROVIDER_INPUT | An input (image, audio) could not be used. |
PROVIDER_INFRA | A temporary failure while generating. Retry. |
PROVIDER_TIMEOUT | Generation took too long. Retry. |
START_FAILED | The job could not start. |
STALE_TIMEOUT | No progress for 75 minutes. |
LINE_TOO_LONG | A line was too long for its scene. |
VOICE_UNAVAILABLE | The voice no longer works — re-clone it. |
FIX_NOT_APPLIED | An edit could not be applied. |
MISSING_ASSET | Something the job needed is gone (e.g. the source video). |
AUDIO_TOO_LONG | The speech measured over 59.5 s. |
INTERNAL | Something failed on our side. |
Every failed job is refunded once, automatically.
The workflow_id is wrong or belongs to a different key — existence is never leaked. Ids are returned only once, at queue time; if you lost one, find it in GET /usage/jobs.
Try it
Paste any workflow_id from a video you've queued (or grab one from your job history below):