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.

4 min readSume
All posts

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.

Statuses your loop must handle, docs read 2026-10-08
Sume statusTerminalAction
pendingnosleep, poll again
in_progressnosleep, poll again
completedyesfetch /content
failedyesread the job events, do not retry blindly
cancelledyesstop

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

All Developers posts

Written by Sume