Generate videos, use reference images or videos, poll jobs, and download completed files.
Video Generation
Use POST /videos/generation to start a video generation job. Video generation is asynchronous:
the trigger request returns a jobId, then you poll GET /jobs/{id} until the job has completed.
For the shared polling pattern, see Jobs.
Request shape
{
"modelID": "seedance-2.0",
"prompt": "<PROMPT>"
}Required fields:
| Field | Description |
|---|---|
modelID | Public video model ID configured in PhotoGPT. |
prompt | Text prompt describing the video to generate. |
Common optional fields:
| Field | Description |
|---|---|
numVideos | Number of videos to generate. |
referenceImages | Image URLs with roles such as first frame, last frame, or reference image. |
referenceVideos | Video URLs with reference_video role. |
elementIDs | Existing elements/assets to include when supported by the selected model. |
options.veo | Veo-specific options. |
options.seedance | Seedance-specific options. |
options.kling | Kling-specific options. |
options.grok | Grok-specific options. |
Send only the option object for the model selected by modelID.
Video model guide
Choose a model based on the kind of motion control, reference input, duration, and resolution you need.
| Model family | Model IDs | Option group |
|---|---|---|
| Veo | veo-3.1, veo-3.1-fast | options.veo |
| Seedance | seedance-1.5-pro, seedance-2.0 | options.seedance |
| Kling video | kling-video-standard, kling-video-pro | options.kling |
| Kling motion | kling-motion-standard, kling-motion-pro | options.kling |
| Grok Imagine | grok-imagine-1.5 | options.grok |
Veo 3.1
Use modelID: "veo-3.1" for high-quality text-to-video or image-to-video jobs.
| Setting | Values or behavior |
|---|---|
| Option group | options.veo |
aspectRatio | 16:9, 9:16. Defaults to 16:9. |
duration | 4, 6, 8. Defaults to 8; must be 8 when using reference_image. |
resolution | 720p, 1080p. Defaults to 720p. |
enhancePrompt | Boolean. Defaults to true. |
generateAudio | Boolean. Defaults to false. |
negativePrompt | Text describing what to avoid. |
seed | Integer for repeatable outputs. |
Veo 3.1 Fast
Use modelID: "veo-3.1-fast" when you want the Veo option surface with faster turnaround.
| Setting | Values or behavior |
|---|---|
| Option group | options.veo |
aspectRatio | 16:9, 9:16. Defaults to 16:9. |
duration | 4, 6, 8. Defaults to 8; must be 8 when using reference_image. |
resolution | 720p, 1080p. Defaults to 720p. |
enhancePrompt | Boolean. Defaults to true. |
generateAudio | Boolean. Defaults to false. |
negativePrompt | Text describing what to avoid. |
seed | Integer for repeatable outputs. |
Seedance 1.5 Pro
Use modelID: "seedance-1.5-pro" for Seedance generation when you need draft mode or automatic
duration.
| Setting | Values or behavior |
|---|---|
| Option group | options.seedance |
resolution | 480p, 720p, 1080p. Draft mode forces 480p. |
ratio | 16:9, 4:3, 1:1, 3:4, 9:16, 21:9, adaptive. |
duration | -1 for auto, or 4 to 12. |
draftMode | Boolean. Only supported by seedance-1.5-pro; forces 480p. |
generateAudio | Boolean. |
returnLastFrame | Boolean. |
seed | Integer for repeatable outputs. |
reference_image inputs are dropped by validation for seedance-1.5-pro.
Seedance 2.0
Use modelID: "seedance-2.0" for Seedance jobs that need image or video references.
| Setting | Values or behavior |
|---|---|
| Option group | options.seedance |
resolution | 480p, 720p, 1080p. |
ratio | 16:9, 4:3, 1:1, 3:4, 9:16, 21:9, adaptive. |
duration | 4 to 15. |
referenceImages | Up to 9 reference_image inputs. |
referenceVideos | Up to 3 reference_video inputs. |
generateAudio | Boolean. |
returnLastFrame | Boolean. |
seed | Integer for repeatable outputs. |
Kling Video Standard
Use modelID: "kling-video-standard" for standard Kling text-to-video or image-to-video jobs.
| Setting | Values or behavior |
|---|---|
| Option group | options.kling |
duration | 3 to 15. Defaults to 10. |
aspectRatio | 16:9, 9:16, 1:1. Text-to-video only; ignored for image-to-video or elements. |
generateAudio | Boolean. Defaults to false. |
elementIDs | Supported where available. Kling accepts at most 4 total element/reference items. |
Kling Video Pro
Use modelID: "kling-video-pro" when you want the higher-capability Kling video model.
| Setting | Values or behavior |
|---|---|
| Option group | options.kling |
duration | 3 to 15. Defaults to 10. |
aspectRatio | 16:9, 9:16, 1:1. Text-to-video only; ignored for image-to-video or elements. |
generateAudio | Boolean. Defaults to false. |
elementIDs | Supported where available. Kling accepts at most 4 total element/reference items. |
Kling Motion Standard
Use modelID: "kling-motion-standard" for motion-control workflows from existing visual elements or
references.
| Setting | Values or behavior |
|---|---|
| Option group | options.kling |
characterOrientation | image, video. Defaults to video. |
keepOriginalSound | Boolean. Defaults to true. |
elementIDs | At most 4 total items after combining elements, reference images, and videos. |
Kling motion-control models do not accept duration, aspectRatio, or generateAudio.
Kling Motion Pro
Use modelID: "kling-motion-pro" when you need the higher-capability Kling motion-control model.
| Setting | Values or behavior |
|---|---|
| Option group | options.kling |
characterOrientation | image, video. Defaults to video. |
keepOriginalSound | Boolean. Defaults to true. |
elementIDs | At most 4 total items after combining elements, reference images, and videos. |
Kling motion-control models do not accept duration, aspectRatio, or generateAudio.
Grok Imagine 1.5
Use modelID: "grok-imagine-1.5" for Grok video generation.
| Setting | Values or behavior |
|---|---|
| Option group | options.grok |
duration | 1 to 15. Defaults to 8. |
aspectRatio | 1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3. Defaults to 16:9. |
resolution | 480p, 720p. Defaults to 480p. |
Text-to-video
const API_KEY = '<YOUR_API_KEY>'
const BASE_URL = 'https://developer.photogptai.com/api'
const headers = {
Authorization: `Bearer ${API_KEY}`,
'API-Version': '1',
}
const payload = {
modelID: 'veo-3.1',
prompt: 'cinematic close-up of a skincare bottle rotating on wet stone',
numVideos: 1,
options: {
veo: {
duration: 8,
aspectRatio: '16:9',
resolution: '1080p',
generateAudio: false,
enhancePrompt: true,
negativePrompt: 'blur, low quality, distorted text',
},
},
}
const response = await fetch(`${BASE_URL}/videos/generation`, {
method: 'POST',
headers: {
...headers,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
})
if (!response.ok) {
throw new Error(await response.text())
}
const body = await response.json()
const jobId = body.result.jobId
console.log('Video generation queued:', jobId)import requests
API_KEY = "<YOUR_API_KEY>"
BASE_URL = "https://developer.photogptai.com/api"
headers = {
"Authorization": f"Bearer {API_KEY}",
"API-Version": "1",
}
payload = {
"modelID": "veo-3.1",
"prompt": "cinematic close-up of a skincare bottle rotating on wet stone",
"numVideos": 1,
"options": {
"veo": {
"duration": 8,
"aspectRatio": "16:9",
"resolution": "1080p",
"generateAudio": False,
"enhancePrompt": True,
"negativePrompt": "blur, low quality, distorted text",
},
},
}
response = requests.post(f"{BASE_URL}/videos/generation", headers=headers, json=payload)
response.raise_for_status()
job_id = response.json()["result"]["jobId"]
print("Video generation queued:", job_id)Image-to-video
Use referenceImages when the selected model supports image-to-video or first-frame control.
Do not send backend-only preparation metadata such as containsHuman; the backend handles it.
Supported image roles:
| Role | Description |
|---|---|
first_frame | Use the image as the opening frame. |
last_frame | Use the image as the ending frame. |
reference_image | Use the image as visual guidance. |
const payload = {
modelID: 'seedance-2.0',
prompt: 'slow push-in camera movement, soft studio lighting',
numVideos: 1,
referenceImages: [
{
url: 'https://example.com/product-frame.png',
role: 'first_frame',
},
],
options: {
seedance: {
duration: 6,
ratio: '16:9',
resolution: '1080p',
generateAudio: false,
returnLastFrame: true,
},
},
}
const response = await fetch(`${BASE_URL}/videos/generation`, {
method: 'POST',
headers: {
...headers,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
})
if (!response.ok) {
throw new Error(await response.text())
}
const body = await response.json()
const jobId = body.result.jobIdpayload = {
"modelID": "seedance-2.0",
"prompt": "slow push-in camera movement, soft studio lighting",
"numVideos": 1,
"referenceImages": [
{
"url": "https://example.com/product-frame.png",
"role": "first_frame",
},
],
"options": {
"seedance": {
"duration": 6,
"ratio": "16:9",
"resolution": "1080p",
"generateAudio": False,
"returnLastFrame": True,
},
},
}
response = requests.post(f"{BASE_URL}/videos/generation", headers=headers, json=payload)
response.raise_for_status()
job_id = response.json()["result"]["jobId"]Reference video
Use referenceVideos when the selected model accepts a video as motion or style reference.
Do not send backend-only preparation metadata such as duration; the backend computes it when
needed.
const payload = {
modelID: 'seedance-2.0',
prompt: 'match the reference camera motion with a new product subject',
numVideos: 1,
referenceVideos: [
{
url: 'https://example.com/reference-motion.mp4',
role: 'reference_video',
},
],
options: {
seedance: {
duration: 8,
ratio: '16:9',
resolution: '720p',
generateAudio: false,
},
},
}
const response = await fetch(`${BASE_URL}/videos/generation`, {
method: 'POST',
headers: {
...headers,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
})
if (!response.ok) {
throw new Error(await response.text())
}
const body = await response.json()
const jobId = body.result.jobIdpayload = {
"modelID": "seedance-2.0",
"prompt": "match the reference camera motion with a new product subject",
"numVideos": 1,
"referenceVideos": [
{
"url": "https://example.com/reference-motion.mp4",
"role": "reference_video",
},
],
"options": {
"seedance": {
"duration": 8,
"ratio": "16:9",
"resolution": "720p",
"generateAudio": False,
},
},
}
response = requests.post(f"{BASE_URL}/videos/generation", headers=headers, json=payload)
response.raise_for_status()
job_id = response.json()["result"]["jobId"]Poll the job
Poll GET /jobs/{id} until the job reaches a terminal status. The helper below follows the
same pattern described in Jobs.
async function waitForJob(jobId) {
const runningStatuses = new Set(['created', 'queued', 'running'])
while (true) {
const response = await fetch(`${BASE_URL}/jobs/${jobId}`, {
headers,
})
if (!response.ok) {
throw new Error(await response.text())
}
const body = await response.json()
const job = body.result
const status = String(job.status ?? '').toLowerCase()
if (!runningStatuses.has(status)) {
return job
}
await new Promise((resolve) => setTimeout(resolve, 8000))
}
}
const job = await waitForJob(jobId)
const status = String(job.status ?? '').toLowerCase()
if (status === 'success') {
for (const video of job.videos ?? []) {
console.log(video.id, video.url)
}
} else {
throw new Error(job.error ?? `Video generation failed: ${job.status}`)
}import time
def wait_for_job(job_id: str):
running_statuses = {"created", "queued", "running"}
while True:
response = requests.get(f"{BASE_URL}/jobs/{job_id}", headers=headers)
response.raise_for_status()
job = response.json()["result"]
status = job.get("status", "").lower()
if status not in running_statuses:
return job
time.sleep(8)
job = wait_for_job(job_id)
if job.get("status", "").lower() == "success":
for video in job.get("videos", []):
print(video["id"], video["url"])
else:
raise RuntimeError(job.get("error") or f"Video generation failed: {job.get('status')}")List and inspect videos
Use GET /videos to list completed videos.
Omit cursor or send an empty string for the first page. When hasMore is true,
send the returned nextCursor as cursor to fetch the next page. Keep
filters unchanged between pages; restart with an empty cursor when they change.
pageSize defaults to 20 and accepts values from 1 to 100.
const params = new URLSearchParams({
videoType: 'all',
cursor: '',
pageSize: '20',
})
const response = await fetch(`${BASE_URL}/videos?${params}`, {
headers,
})
if (!response.ok) {
throw new Error(await response.text())
}
const body = await response.json()
const videos = body.result
const nextCursor = body.hasMore ? body.nextCursor : nullresponse = requests.get(
f"{BASE_URL}/videos",
headers=headers,
params={"videoType": "all", "cursor": "", "pageSize": 20},
)
response.raise_for_status()
body = response.json()
videos = body["result"]
next_cursor = body["nextCursor"] if body["hasMore"] else NoneUse GET /videos/{videoID} to inspect one video.
const videoId = '<VIDEO_ID>'
const response = await fetch(`${BASE_URL}/videos/${videoId}`, {
headers,
})
if (!response.ok) {
throw new Error(await response.text())
}
const body = await response.json()
const video = body.resultvideo_id = "<VIDEO_ID>"
response = requests.get(f"{BASE_URL}/videos/{video_id}", headers=headers)
response.raise_for_status()
video = response.json()["result"]Download a video
Video downloads are prepared in two steps.
First, request the download:
const videoId = '<VIDEO_ID>'
const response = await fetch(`${BASE_URL}/videos/${videoId}/create-download`, {
headers,
})
if (!response.ok) {
throw new Error(await response.text())
}video_id = "<VIDEO_ID>"
response = requests.get(f"{BASE_URL}/videos/{video_id}/create-download", headers=headers)
response.raise_for_status()Then poll the download endpoint until the status is ready. The current download status values
are inprogress, ready, and error.
while (true) {
const response = await fetch(`${BASE_URL}/videos/${videoId}/download`, {
headers,
})
if (!response.ok) {
throw new Error(await response.text())
}
const body = await response.json()
const download = body.result
if (download.status === 'ready') {
console.log(download.url)
break
}
if (download.status === 'error') {
throw new Error('Video download preparation failed.')
}
console.log('Preparing download:', download.percentComplete ?? 0)
await new Promise((resolve) => setTimeout(resolve, 5000))
}import time
while True:
response = requests.get(f"{BASE_URL}/videos/{video_id}/download", headers=headers)
response.raise_for_status()
download = response.json()["result"]
if download.get("status") == "ready":
print(download["url"])
break
if download.get("status") == "error":
raise RuntimeError("Video download preparation failed.")
print("Preparing download:", download.get("percentComplete", 0))
time.sleep(5)