Swift: URLSession async/await for one 30-second Wan 3.0 job
A 28-line main.swift that submits wan-3.0 for 30 seconds, polls with Task.sleep and saves the MP4. Runs on macOS or Linux with swiftc.

A single 30-second Wan 3.0 job from Swift is one main.swift file: URLSession with async/await, JSONSerialization for the two fields you need, and Task.sleep between polls. The request below asks for 9:16 at 720p, reserved at $3.75.
main.swift
Top-level await works in main.swift, so no @main type is needed. Compile with swiftc main.swift or run it with swift main.swift.
import Foundation
let key = ProcessInfo.processInfo.environment["SUME_API_KEY"] ?? ""
guard !key.isEmpty else { fatalError("SUME_API_KEY is empty") }
let base = "https://api.sume.com/v1/videos"
func send(_ m: String, _ url: String, _ body: [String: Any]? = nil, idem: String? = nil) async throws -> Data {
var req = URLRequest(url: URL(string: url)!)
req.httpMethod = m
req.setValue("Bearer \(key)", forHTTPHeaderField: "Authorization")
if let body {
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.httpBody = try JSONSerialization.data(withJSONObject: body)
}
if let idem { req.setValue(idem, forHTTPHeaderField: "Idempotency-Key") }
let (data, resp) = try await URLSession.shared.data(for: req)
guard (resp as! HTTPURLResponse).statusCode < 300 else { fatalError("request failed") }
return data
}
func json(_ d: Data) -> [String: Any] { try! JSONSerialization.jsonObject(with: d) as! [String: Any] }
let id = json(try await send("POST", base, [
"model": "wan-3.0", "prompt": "A slow orbit around a ceramic mug on a sunlit desk",
"duration": 30, "resolution": "720p", "aspect_ratio": "9:16"], idem: "wan-swift-001"))["id"] as! String
var status = "pending"
while status == "pending" || status == "in_progress" {
try await Task.sleep(nanoseconds: 5_000_000_000)
status = json(try await send("GET", "\(base)/\(id)"))["status"] as! String
}
guard status == "completed" else { fatalError("job ended as \(status)") }
try await send("GET", "\(base)/\(id)/content?index=0").write(to: URL(fileURLWithPath: "clip.mp4"))What to notice
- The helper accepts an optional body and an optional idempotency key, so the submit and the two reads share one function.
fatalErroron a status of 300 or above ends the process on a402,409or429. In an app, map those to user-facing messages instead.- A
409 idempotency_conflictmeans the key was reused for a different payload. Reuse a key only for an exact retry.
Cost of what you pass in
The reserve moves with resolution and duration, nothing else:
| resolution | 10 s | 20 s | 30 s |
|---|---|---|---|
| 480p | $0.625 | $1.25 | $1.875 |
| 720p | $1.25 | $2.50 | $3.75 |
| 1080p | $2.50 | $5.00 | $7.50 |
Smoke test before the 30-second run
Run the script once with "duration": 2 and "resolution": "480p" before you trust it. Wan 3.0 accepts 2 seconds, and that job reserves $0.125, against $3.75 for the full 720p clip. It proves the key, the polling and the file write for the price of a coffee-sized fraction of one real run. Swift's fatalError will print nothing useful on a bad key, so the cheap run is also your error-path check.
Using it from an app
Do not ship the API key in an iOS or macOS app bundle. Put this code behind your own server, and let the app call your server. For batches of clips, see the existing post on running several submits at once with a task group.
Sources
Related posts
More in Developers
- Switch video models by changing one string: what can still break
On Sume's /v1/videos you swap the model id and keep the body. Duration range, resolution and aspect ratio are the three fields that may need adjusting.
- sync, subscribe, async or webhook: which Sume mode for a video job
Sume's sync and subscribe modes wait at most 30 seconds, then return a job id. Use async or webhook for video; Python that survives a timed-out wait.
- rate_limited or queue_full? One Python submit handler for both 429s
Two different 429s need two different waits. A Python handler reads error.code, sleeps on retry-after for rate_limited, and waits for capacity on queue_full.
- Test one video prompt on ten models for under $7 (Python sweep)
A 5-second, lowest-resolution sweep of one prompt over ten Sume video ids costs about $5.60 in total. The price of each row and a script that submits them.
Written by Sume