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.

4 min readSume
All posts

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-Key makes 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_SECONDS so a test can set it to 0.
  • failed and cancelled are terminal; the loop stops with the error field 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.

kling-3 request limits in the Sume model card (read 2026-10-05)
FieldAccepted values
resolution720p, 1080p
duration4 to 15 seconds, whole numbers
aspect_ratio16:9, 9:16, 1:1
generate_audiotrue (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

All Developers posts

Written by Sume