PHP: submit a 30-second Wan 3.0 clip, poll it, save the MP4

Plain PHP 8 with curl and no SDK: send a 30-second wan-3.0 request, poll the job, and write the MP4 to disk. The 720p run is reserved at $3.75.

3 min readSume
All posts

You can drive a 30-second Wan 3.0 clip from plain PHP 8 with curl: POST to /v1/videos with "model": "wan-3.0" and "duration": 30, poll GET /v1/videos/{id} until the status is completed, then save /content?index=0. At 720p Sume reserves $3.75 for the job. Alibaba lists wan3.0-video at 2 to 30 seconds and 30 fps MP4 (read 2026-10-08), and the Video generation docs give the same 2 to 30 second range for the wan-3.0 id.

The script

One helper does every call. CURLOPT_FAILONERROR turns any HTTP 4xx or 5xx into a failed request, so a 402 insufficient_credits stops the script instead of being parsed as a job.

<?php
$base = "https://api.sume.com";
$key = getenv("SUME_API_KEY") ?: exit("SUME_API_KEY is empty\n");
function sume(string $method, string $url, string $key, ?array $body = null, string $idem = ""): string {
    $headers = ["Authorization: Bearer $key", "Content-Type: application/json"];
    if ($idem !== "") $headers[] = "Idempotency-Key: $idem";
    $ch = curl_init($url);
    curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $headers,
        CURLOPT_RETURNTRANSFER => true, CURLOPT_FAILONERROR => true]);
    if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
    $out = curl_exec($ch);
    if ($out === false) exit("request failed: " . curl_error($ch) . "\n");
    return $out;
}
$job = json_decode(sume("POST", "$base/v1/videos", $key, [
    "model" => "wan-3.0", "prompt" => "A fishing boat leaving a harbor at dawn",
    "duration" => 30, "resolution" => "720p", "aspect_ratio" => "16:9",
], "wan-php-001"), true);
do {
    sleep(5);
    $s = json_decode(sume("GET", "$base/v1/videos/{$job['id']}", $key), true);
} while (in_array($s["status"], ["pending", "in_progress"], true));
if ($s["status"] !== "completed") exit("ended as {$s['status']}\n");
file_put_contents("clip.mp4", sume("GET", "$base/v1/videos/{$job['id']}/content?index=0", $key));

What each piece is for

  • Idempotency-Key on the POST only: a retry of the same key returns the original job instead of a second charge. Change the key for a different request.
  • duration is an integer. Send 30, not 30.0 or "30s".
  • The poll loop treats pending and in_progress as running and everything else as terminal; failed and cancelled fall through to the exit message.

Price before you run it

The reserve is flat per second, so the whole table is known before you send anything.

Sume list price for wan-3.0, read 2026-10-08
ResolutionPer second30-second clip
480p$0.0625$1.875
720p$0.125$3.75
1080p$0.25$7.50

Failure cases to handle

Three responses matter for a paid submit.

  • 402 insufficient_credits: the balance cannot cover the reserve. Submit a cheaper request, or change plan, then retry with a fresh key if the request changed.
  • 409 idempotency_conflict: the key was used for a different payload. Mint a new key for a genuinely new request.
  • 429: the workspace has no accepted-job capacity right now. Wait, then retry with the same key; the Generation admission page lists the codes.

Before production

Replace the five-second sleep with a callback_url if the script runs under a web request limit; the callback must be HTTPS. Keep the key in the environment, never in the file, and log the job id next to the prompt so a cost line can be traced to its request.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume