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.

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.
| Step | Call | Fields |
|---|---|---|
| Submit | POST /v1/videos | model, prompt, duration in; id, polling_url, status, model out |
| Poll | GET /v1/videos/{id} | status: pending, in_progress, completed, failed, cancelled |
| Fetch | GET /v1/videos/{id}/content | 302 to the artifact; 409 job_not_completed or job_failed |
| Auth | Authorization: Bearer | one 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
- Env and secrets diff for removing Sora: SUME_API_KEY, one auth header
Which environment variables to delete, add and rotate when a service leaves the OpenAI Videos API for Sume, plus a startup check that fails on a missing secret.
- Extract 16 kHz mono audio from a video for transcription
A Python script that detaches speech-ready 16 kHz mono wav from a Sume-hosted video, then polls the job. Costs $0.01 per detach before the STT minute.
- Face swap request in Python: validate the video URL before you send
A Python snippet that rejects non-HTTPS, localhost and private-IP video URLs locally, then submits a Sume face-swap beta run with a quality and idempotency key.
- Failed Sume video job: resubmit or stop? Read retryable, next_action
Sora said failed and little else. A failed Sume job carries an error with category, retryable, retry_after_seconds and next_action. A small decision function.
Written by Sume