Kotlin: submit and poll a Sume video job with java.net.http
A Kotlin port of a Sora videos call: POST /v1/videos with an Idempotency-Key, poll every 30 seconds until done, with the JDK client and kotlinx.serialization.

For a Kotlin backend that called Sora over HTTP, the replacement is a blocking submit and poll with the JDK's java.net.http client and kotlinx.serialization for the JSON: POST /v1/videos returns 202 with an id, then GET /v1/videos/{id} every 30 seconds until the status is completed, failed or cancelled.
OpenAI's deprecations page, read 2026-10-08, lists the Videos API as removed on September 24, 2026 with an empty replacement column, so this is a port to a different vendor, not a rename.
Status words
Poll on the Sume vocabulary. A Sora loop that stopped on completed or failed will spin forever on cancelled if you forget it.
| Sume status | Terminal | Action |
|---|---|---|
| pending | no | sleep, poll again |
| in_progress | no | sleep, poll again |
| completed | yes | fetch /content |
| failed | yes | read the job events, do not retry blindly |
| cancelled | yes | stop |
The code
Add org.jetbrains.kotlinx:kotlinx-serialization-json to the build, and set SUME_API_KEY. The send helper throws on any non-2xx so a 402 or 429 is loud.
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import kotlinx.serialization.json.*
val http: HttpClient = HttpClient.newHttpClient()
val key: String = System.getenv("SUME_API_KEY") ?: error("SUME_API_KEY is not set")
fun send(builder: HttpRequest.Builder): JsonObject {
val req = builder.header("Authorization", "Bearer $key").build()
val res = http.send(req, HttpResponse.BodyHandlers.ofString())
check(res.statusCode() in 200..299) { "HTTP ${res.statusCode()}: ${res.body()}" }
return Json.parseToJsonElement(res.body()).jsonObject
}
fun main() {
val body = """{"model":"sume/auto","prompt":"A kite over a harbor","duration":8}"""
var job = send(HttpRequest.newBuilder(URI("https://api.sume.com/v1/videos"))
.header("Content-Type", "application/json")
.header("Idempotency-Key", "kite-1")
.POST(HttpRequest.BodyPublishers.ofString(body)))
val id = job["id"]!!.jsonPrimitive.content
while (job["status"]!!.jsonPrimitive.content in setOf("pending", "in_progress")) {
Thread.sleep(30_000)
job = send(HttpRequest.newBuilder(URI("https://api.sume.com/v1/videos/$id")).GET())
}
println(job["status"]!!.jsonPrimitive.content)
}Where to take it next
Thread.sleep blocks a thread, which is fine in a worker but not in a request handler. Run the poll in a background job and store the Sume id, or use a webhook through the callback_url field and skip polling; the polling versus callback post compares the two.
Choose the Idempotency-Key from your own record id, not a random value, so a redelivered queue message cannot create a second billable job.
Sources
Related posts
More in Developers
- Kubernetes CronJob that submits a nightly 30-second Wan 3.0 clip
A CronJob manifest using curlimages/curl and a Secret: one dated Idempotency-Key per night, concurrency forbidden, and a month of reserves at each resolution.
- List your Sume Formats in Python and keep the video ones
Page through GET /v1/formats with next_cursor, keep io.output_kind video, and print each vanity_invoke_url. A runnable snippet with field caveats.
- MAI-Voice-2.1 SSML style="happiness" is not in the voice style list
Microsoft's MAI-Voice-2.1 SSML example uses style="happiness", but the Learn page's own style lists say happy or joyful. Check styles before you ship.
- Modal 1.6.1 endpoint logs and stats: debug a Sume webhook receiver
Modal 1.6.1 adds modal endpoint info, stats and logs. Use them to see why a Sume job webhook got a 401 or a timeout, and check the 150 s web timeout first.
Written by Sume