PHP cURL: submit a Seedance 2.5 video job, poll it, save the MP4
PHP with only ext-curl: POST /v1/videos for seedance-2.5, poll polling_url until completed, then download content?index=0; a 5-second 480p test costs $1.34.

With plain PHP cURL you submit POST https://api.sume.com/v1/videos, read polling_url from the 202 response, poll it until status is completed, and download GET /v1/videos/{id}/content?index=0 with the same bearer key. A 5-second 480p seedance-2.5 test clip costs $1.34 on Sume (Video generation, read 2026-10-05).
What does the whole script look like?
This uses only the cURL extension and json_decode, with no Composer packages. Set SUME_API_KEY in the environment first.
<?php
$key = getenv('SUME_API_KEY');
$headers = ["Authorization: Bearer $key", 'Content-Type: application/json'];
function call($url, $headers, $body = null) {
$ch = curl_init($url);
curl_setopt_array($ch, [CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) { curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body)); }
$out = curl_exec($ch);
curl_close($ch);
return json_decode($out, true);
}
$job = call('https://api.sume.com/v1/videos', $headers, [
'model' => 'seedance-2.5',
'prompt' => 'A paper boat drifts down a rain gutter, soft overcast light',
'duration' => 5, 'resolution' => '480p', 'aspect_ratio' => '16:9',
]);
do {
sleep(10);
$s = call($job['polling_url'], $headers);
echo $s['status'], "\n";
} while (in_array($s['status'], ['pending', 'in_progress'], true));
if ($s['status'] === 'completed') {
$ch = curl_init("https://api.sume.com/v1/videos/{$job['id']}/content?index=0");
curl_setopt_array($ch, [CURLOPT_HTTPHEADER => $headers, CURLOPT_FOLLOWLOCATION => true, CURLOPT_RETURNTRANSFER => true]);
file_put_contents('clip.mp4', curl_exec($ch));
}
Which statuses do I handle?
The loop stops on any status other than pending or in_progress. Read the error field on failed.
| Status | Meaning | What the script does |
|---|---|---|
| pending | queued | keeps polling |
| in_progress | generating | keeps polling |
| completed | ready to download | downloads the MP4 |
| failed | generation failed, see error | stops; no file |
| cancelled | canceled before completing | stops; no file |
What about retries and callbacks?
Add an Idempotency-Key header on the submit call so a timeout-and-retry returns the original job instead of a second charge. If you would rather not poll, send an HTTPS callback_url and verify the signed webhook; the signature header is x-sume-webhook-signature. For the same flow in another language, see the Ruby version.
Sources
Related posts
More in Developers
- Pick a Sume video model by script: filter /v1/videos/models
Instead of guessing, call GET /v1/videos/models and filter by ratio, resolution and last frame. A 15-line Python script lists the Sume video models that fit.
- Pick an image model by capability from the Sume catalog in Python
Filter GET /v1/images/models by reference count, ratio and resolution tier instead of hard-coding ids. A short Python function that runs as written.
- Pin the Sume CLI to a release tag in CI and check for updates
Install a fixed sume binary from GitHub Releases, verify checksums.txt, and run sume update --check to see a newer release without changing files.
- Pinterest reads IPTC metadata for AI labels: keep it before you pin
Pinterest says its AI detection follows the IPTC Photo Metadata Standard. If you tag a Sume image, keep that metadata; Sume's docs do not say it embeds any.
Written by Sume