PhotoGPT Developers

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:

FieldDescription
modelIDPublic video model ID configured in PhotoGPT.
promptText prompt describing the video to generate.

Common optional fields:

FieldDescription
numVideosNumber of videos to generate.
referenceImagesImage URLs with roles such as first frame, last frame, or reference image.
referenceVideosVideo URLs with reference_video role.
elementIDsExisting elements/assets to include when supported by the selected model.
options.veoVeo-specific options.
options.seedanceSeedance-specific options.
options.klingKling-specific options.
options.grokGrok-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 familyModel IDsOption group
Veoveo-3.1, veo-3.1-fastoptions.veo
Seedanceseedance-1.5-pro, seedance-2.0options.seedance
Kling videokling-video-standard, kling-video-prooptions.kling
Kling motionkling-motion-standard, kling-motion-prooptions.kling
Grok Imaginegrok-imagine-1.5options.grok

Veo 3.1

Use modelID: "veo-3.1" for high-quality text-to-video or image-to-video jobs.

SettingValues or behavior
Option groupoptions.veo
aspectRatio16:9, 9:16. Defaults to 16:9.
duration4, 6, 8. Defaults to 8; must be 8 when using reference_image.
resolution720p, 1080p. Defaults to 720p.
enhancePromptBoolean. Defaults to true.
generateAudioBoolean. Defaults to false.
negativePromptText describing what to avoid.
seedInteger for repeatable outputs.

Veo 3.1 Fast

Use modelID: "veo-3.1-fast" when you want the Veo option surface with faster turnaround.

SettingValues or behavior
Option groupoptions.veo
aspectRatio16:9, 9:16. Defaults to 16:9.
duration4, 6, 8. Defaults to 8; must be 8 when using reference_image.
resolution720p, 1080p. Defaults to 720p.
enhancePromptBoolean. Defaults to true.
generateAudioBoolean. Defaults to false.
negativePromptText describing what to avoid.
seedInteger for repeatable outputs.

Seedance 1.5 Pro

Use modelID: "seedance-1.5-pro" for Seedance generation when you need draft mode or automatic duration.

SettingValues or behavior
Option groupoptions.seedance
resolution480p, 720p, 1080p. Draft mode forces 480p.
ratio16:9, 4:3, 1:1, 3:4, 9:16, 21:9, adaptive.
duration-1 for auto, or 4 to 12.
draftModeBoolean. Only supported by seedance-1.5-pro; forces 480p.
generateAudioBoolean.
returnLastFrameBoolean.
seedInteger 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.

SettingValues or behavior
Option groupoptions.seedance
resolution480p, 720p, 1080p.
ratio16:9, 4:3, 1:1, 3:4, 9:16, 21:9, adaptive.
duration4 to 15.
referenceImagesUp to 9 reference_image inputs.
referenceVideosUp to 3 reference_video inputs.
generateAudioBoolean.
returnLastFrameBoolean.
seedInteger for repeatable outputs.

Kling Video Standard

Use modelID: "kling-video-standard" for standard Kling text-to-video or image-to-video jobs.

SettingValues or behavior
Option groupoptions.kling
duration3 to 15. Defaults to 10.
aspectRatio16:9, 9:16, 1:1. Text-to-video only; ignored for image-to-video or elements.
generateAudioBoolean. Defaults to false.
elementIDsSupported 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.

SettingValues or behavior
Option groupoptions.kling
duration3 to 15. Defaults to 10.
aspectRatio16:9, 9:16, 1:1. Text-to-video only; ignored for image-to-video or elements.
generateAudioBoolean. Defaults to false.
elementIDsSupported 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.

SettingValues or behavior
Option groupoptions.kling
characterOrientationimage, video. Defaults to video.
keepOriginalSoundBoolean. Defaults to true.
elementIDsAt 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.

SettingValues or behavior
Option groupoptions.kling
characterOrientationimage, video. Defaults to video.
keepOriginalSoundBoolean. Defaults to true.
elementIDsAt 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.

SettingValues or behavior
Option groupoptions.grok
duration1 to 15. Defaults to 8.
aspectRatio1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3. Defaults to 16:9.
resolution480p, 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:

RoleDescription
first_frameUse the image as the opening frame.
last_frameUse the image as the ending frame.
reference_imageUse 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.jobId
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,
        },
    },
}

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.jobId
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,
        },
    },
}

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 : null
response = 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 None

Use 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.result
video_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)

API reference

On this page