Elixir Req: submit and poll a Sume video job after Sora

An Elixir script using Req: POST /v1/videos with an Idempotency-Key, poll every 30 seconds, stop on completed, failed or cancelled. Req never retries the POST.

4 min readSume
All posts

In Elixir, Req gives you the whole Sora replacement in about fifteen lines: a client with base_url https://api.sume.com and a bearer token, one post to /v1/videos with an idempotency-key header, and a recursive function that sleeps 30 seconds and reads /v1/videos/{id} until the status leaves pending and in_progress.

The OpenAI deprecations page, read 2026-10-08, lists the Videos API as removed on September 24, 2026 with no replacement named, so your Elixir code needs a new vendor rather than a new version.

What Req does for you

Req decodes JSON responses into maps, so job["status"] works without a decoder step. Its default retry policy is safe_transient, which retries GET and HEAD only, so the POST that creates a billable job is not repeated behind your back. You still send an Idempotency-Key, because your own supervisor might re-run the function.

Request and response fields used here, Sume docs read 2026-10-08
StepCallFields
SubmitPOST /v1/videosmodel, prompt, duration in; id, polling_url, status, model out
PollGET /v1/videos/{id}status: pending, in_progress, completed, failed, cancelled
FetchGET /v1/videos/{id}/content302 to the artifact; 409 job_not_completed or job_failed
AuthAuthorization: Bearerone credential only, never Bearer and x-api-key together

The script

Save it as sume.exs, export SUME_API_KEY and run elixir sume.exs. Mix.install fetches Req the first time.

Mix.install([{:req, "~> 0.5"}])

key = System.fetch_env!("SUME_API_KEY")
req = Req.new(base_url: "https://api.sume.com", auth: {:bearer, key})

submit =
  Req.post!(req,
    url: "/v1/videos",
    headers: [{"idempotency-key", "fox-1"}],
    json: %{model: "sume/auto", prompt: "A fox crossing a snowy field", duration: 8}
  )

202 = submit.status
job = submit.body

wait = fn wait, id ->
  Process.sleep(30_000)
  body = Req.get!(req, url: "/v1/videos/#{id}").body

  if body["status"] in ["pending", "in_progress"],
    do: wait.(wait, id),
    else: body
end

final =
  if job["status"] in ["pending", "in_progress"], do: wait.(wait, job["id"]), else: job

IO.puts(final["status"])

Going further

The pattern match 202 = submit.status crashes the script on a 402, 429 or 400, which is what you want in a one-off; in an Oban worker, match on the status and return an error tuple so the job backs off. Sume says rate_limited and queue_full are 429, and the response carries retry-after.

For three status vocabularies you may meet when you also read job events, see the normalizer post.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume