Ruby Net::HTTP never follows redirects: saving a Sume video
Net::HTTP returns the 302 from /v1/videos/{id}/content as-is. A 26-line Ruby script submits Seedance 2.5, polls, follows the redirect and saves the MP4.

A Ruby script that polls a Sume video job to completed and then saves an empty or tiny file has almost always hit the same thing: Ruby's Net::HTTP does not follow redirects. The download URL in unsigned_urls[0] is /v1/videos/{id}/content?index=0, and Sume answers it with a 302 to the artifact. The standard library hands you that 302 response and stops.
This script submits a Seedance 2.5 job, polls it, follows the redirect by hand and writes out.mp4. It is 26 lines of plain Ruby with no gems. It was run against a local stand-in server that returns the documented 202, poll and 302 shapes.
The script
Set SUME_API_KEY and run ruby video.rb. The request is an 8-second, 720p, 9:16 Seedance 2.5 clip, which Sume prices per video token. By the pricing table that is about $4.62 for 8 seconds in 720p vertical; the exact figure arrives in usage.cost on the completed job.
require "net/http"
require "json"
BASE = ENV.fetch("SUME_API_BASE", "https://api.sume.com")
KEY = ENV.fetch("SUME_API_KEY")
def send_req(req, uri)
req["Authorization"] = "Bearer #{KEY}"
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |h| h.request(req) }
end
uri = URI("#{BASE}/v1/videos")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
req["Idempotency-Key"] = "seedance-25-demo-0001"
req.body = { model: "seedance-2.5", prompt: "A paper boat drifts down a rainy street",
duration: 8, resolution: "720p", aspect_ratio: "9:16" }.to_json
poll = URI(JSON.parse(send_req(req, uri).body).fetch("polling_url"))
loop do
sleep ENV.fetch("POLL_SECONDS", "30").to_i
job = JSON.parse(send_req(Net::HTTP::Get.new(poll), poll).body)
abort "#{job['status']}: #{job['error']}" if %w[failed cancelled].include?(job["status"])
next unless job["status"] == "completed"
url = URI(job["unsigned_urls"][0])
res = send_req(Net::HTTP::Get.new(url), url)
res = Net::HTTP.get_response(URI(res["location"])) if res.is_a?(Net::HTTPRedirection)
File.binwrite("out.mp4", res.body)
puts "saved, usage.cost = #{job.dig('usage', 'cost')}"
break
endThe three details that matter
Most of the script is ordinary. These parts are where Ruby readers get stuck.
res.is_a?(Net::HTTPRedirection)is the check for the 302. Thelocationheader holds the next URL, and the second request goes out without the Bearer header because the artifact link is not the API. Sending credentials to a storage host you do not control is a habit worth avoiding, and it keeps the script correct if the redirect target changes.Idempotency-Keyon the submit means a retry after a network error returns the original job. Without it a second POST can reserve and spend a second time.failedandcancelledare terminal. The script aborts with theerrorfield, and a 409job_not_completedfrom the content URL only means you asked too early.
Seedance 2.5 limits to know
Seedance 2.5 runs 4 to 30 seconds at 480p, 720p or 1080p, with eight aspect ratio values including auto. It accepts image, video and audio references, which Kling 3.0 does not. The price is a token price, so 1080p costs far more than 720p for the same length.
| Resolution | Output size | Price |
|---|---|---|
| 480p | 496 x 864 | $1.34 |
| 720p | 720 x 1280 | $2.89 |
| 1080p | 1080 x 1920 | $7.11 |
Poll rhythm
Poll about every 30 seconds and read status each time. If you need the finished clip in a queue worker, store the job id first and fetch the content URL only after completed.
Sources
Related posts
More in Developers
- Ruby Net::HTTP: POST to Sume images and save the file
Net::HTTP.start with use_ssl and read_timeout, one POST to /v1/images, a string comparison on res.code, and File.binwrite for the image. No gems needed.
- Run create 503 studio_agent_upstream_timeout: the run may exist
A 502 or 503 on a Sume run create can mean the run already exists and is spending. Retry with the same Idempotency-Key and the original run is replayed.
- Sume run webhook outcome degraded: status OK but output is null
A Sume run webhook can say status OK and still carry output null. Branch on outcome (ok, degraded, error), not status, and dedupe on the envelope request_id.
- First MCP call: Runway whoami vs Sume account_me and mcp_health
Runway says verify its dev MCP with whoami. On Sume, call mcp_health for endpoint and auth source, then account_me for the workspace; both are read only.
Written by Sume