Swift: submit a Kling 3.0 job and save the MP4 to disk
One Swift file: POST /v1/videos for kling-3, poll the polling_url, follow the content redirect and write out.mp4. A 5 s clip without audio costs $0.70.

If you ship an iOS or macOS tool that turns a prompt into a clip, the whole job is four HTTP calls: submit, poll, follow one redirect, write bytes. This post is the complete Swift file for Kling 3.0 (kling-3) on Sume's /v1/videos route, 30 lines, using only Foundation. It ran under a current Swift toolchain, and it relies on top-level await, which main.swift allows.
The request asks for a 5-second, 720p, 9:16 clip with generate_audio set to false. On Sume that is 5 x $0.14 = $0.70. Leave the audio flag out and Kling's audio default applies instead, which is $0.21 a second, or $1.05 for the same clip.
The file
Save it as main.swift, export SUME_API_KEY, and run swift main.swift. The sample was run against a local stand-in server that returns the documented 202, poll and 302 shapes, so the control flow is exercised; the live service decides the actual render time.
import Foundation
let env = ProcessInfo.processInfo.environment
let base = env["SUME_API_BASE"] ?? "https://api.sume.com"
func send(_ url: String, body: [String: Any]? = nil) async throws -> ([String: Any], Data) {
var req = URLRequest(url: URL(string: url)!)
req.setValue("Bearer \(env["SUME_API_KEY"]!)", forHTTPHeaderField: "Authorization")
if let body {
req.httpMethod = "POST"
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.setValue("kling3-demo-0001", forHTTPHeaderField: "Idempotency-Key")
req.httpBody = try JSONSerialization.data(withJSONObject: body)
}
let (data, _) = try await URLSession.shared.data(for: req)
return ((try? JSONSerialization.jsonObject(with: data)) as? [String: Any] ?? [:], data)
}
let (job, _) = try await send("\(base)/v1/videos", body: [
"model": "kling-3", "prompt": "A skateboarder lands a kickflip at dusk",
"duration": 5, "resolution": "720p", "aspect_ratio": "9:16", "generate_audio": false])
while true {
try await Task.sleep(for: .seconds(Double(env["POLL_SECONDS"] ?? "30")!))
let (status, _) = try await send(job["polling_url"] as! String)
let state = status["status"] as? String ?? ""
if ["failed", "cancelled"].contains(state) { fatalError("\(state): \(status["error"] ?? "")") }
if state == "completed" {
let (_, mp4) = try await send((status["unsigned_urls"] as! [String])[0])
try mp4.write(to: URL(fileURLWithPath: "out.mp4"))
print("saved \(mp4.count) bytes, cost \((status["usage"] as? [String: Any])?["cost"] ?? "?")")
break
}
}What each step relies on
The submit call returns 202 with id, polling_url, status and model. The poll body carries status, and once it reads completed it also carries unsigned_urls and usage.cost. The content URL answers with a redirect to the artifact, and the sample's URLSession call received the artifact bytes through it against the stand-in server; if your CDN hop is on another host, check that your client keeps the Bearer header only where you intend.
Idempotency-Keymakes a retried submit return the original job instead of a second charge. Use one key per clip you intend to make.- Poll about every 30 seconds. The sample reads
POLL_SECONDSso a test can set it to 0. failedandcancelledare terminal; the loop stops with theerrorfield instead of waiting forever.- The usage cost on the completed job is the Sume billable amount, which is the provider list price times 1.25.
What Kling 3.0 accepts
Kling 3.0 on Sume takes text-to-video and image-to-video with a start and end frame. It rejects reference_*_urls, so do not send reference arrays to it; use Seedance 2.5 or Wan 3.0 when you need references.
| Field | Accepted values |
|---|---|
| resolution | 720p, 1080p |
| duration | 4 to 15 seconds, whole numbers |
| aspect_ratio | 16:9, 9:16, 1:1 |
| generate_audio | true (default) or false |
| price | $0.14 a second audio off, $0.21 a second audio on |
Before you ship it
Replace the as! casts with real error handling, store the job id before you start polling so a crash does not lose the clip, and write the file through a temporary URL if you have large outputs. The workspace needs enough USD balance to cover the reserve at submit, or the API answers 402 with insufficient_credits.
Sources
Related posts
More in Developers
- Swift URLSession: call Sume's image API and branch on 200 or 202
Use URLSession.data(for:) with async/await, cast the response to HTTPURLResponse, and read statusCode before you touch data[0].url on Sume's /v1/images.
- Which Sume audio calls can sync-wait 30 s? A map vs 150 ms claims
Detach, timeline audio, ingest, music and TTS: which can return in a 30 s sync wait, which are async jobs, and what a 150 ms end-to-end claim leaves out.
- Talking Photo From 3 Minutes of Audio: Fabric Cost and File Size
Turn a still and a 180-second recording into one talking-photo video: Sume Fabric cost at 720p and 480p, the 10 MB audio cap, and WAV vs MP3 size.
- Tcl http package: submit a Gemini Omni video job and follow the 302
A 22-line Tcl script using the http, json and tls packages submits a 3 s 360p Gemini Omni job on Sume ($0.1125), polls it and follows the content redirect.
Written by Sume