Ruby Net::HTTP: POST /v1/videos with callback_url, for a Sora port
A 15-line Net::HTTP submit for /v1/videos that sends an HTTPS callback_url and an Idempotency-Key, checks for 202, and stores the id before any callback.

Post to /v1/videos with a callback_url and an Idempotency-Key, require a 202, and write the returned id to your database before you do anything else. OpenAI's deprecations page lists the Videos API and the sora-2 model ids as removed on September 24, 2026, so a Ruby job that posted there needs a new target, and Sume's /v1/videos is a bare-object, async route that fits the same submit-then-wait shape.
What the response and callback look like
The 202 body is a plain object with id, polling_url, status and model. Status values on this route are pending, in_progress, completed, failed and cancelled. The callback carries Sume's standard job webhook envelope, not an OpenRouter-style event, and is signed with x-sume-webhook-signature.
| Item | Value |
|---|---|
| Success status | 202 |
| callback_url | HTTPS only |
| Events | job.completed, job.failed, job.canceled |
| Signature header | x-sume-webhook-signature: sume-v1=... |
| Retry safety | Idempotency-Key; a replay returns the original job |
The submit
Run it with SUME_API_KEY set. The prompt, model and hooks.example.com URL are placeholders. abort on anything but 202 keeps a failed submit from storing a bad id.
require "json"
require "net/http"
require "uri"
base = URI(ENV.fetch("SUME_BASE", "https://api.sume.com"))
http = Net::HTTP.new(base.host, base.port)
http.use_ssl = base.scheme == "https"
req = Net::HTTP::Post.new("/v1/videos", "Authorization" => "Bearer #{ENV.fetch('SUME_API_KEY')}",
"Content-Type" => "application/json", "Idempotency-Key" => "sora-port-0001")
req.body = JSON.generate(model: "seedance-2.5", prompt: "paper boat on a rain puddle", duration: 8,
resolution: "720p", aspect_ratio: "9:16", callback_url: "https://hooks.example.com/sume")
res = http.request(req)
abort "submit failed: #{res.code}" unless res.code == "202"
job = JSON.parse(res.body)
puts job["id"], job["polling_url"] # store the id first, then wait for the callbackWhat a callback does not replace
Delivery is at least once, with up to 10 attempts, so your receiver should dedupe on job_id. Keep polling_url as a fallback for a delivery that never arrives; the docs say a webhook is a delivery optimization, not your only recovery path.
Nothing here ports Sora prompts or settings. Sume does not accept seed or size on this route, and a model's duration and resolution come from GET /v1/videos/models, so expect to adjust the body, not just the URL.
Sources
Related posts
More in Developers
- Ruby Net::HTTP: submit a Wan 3.0 video job and poll until it finishes
A standard-library Ruby script that posts to /v1/videos with wan-3.0, polls the polling_url every 30 seconds and stops on completed, failed or cancelled.
- Scan your repo for retiring Google image and TTS ids (Python)
A 30-line Python scanner that finds Imagen 4, Nano Banana and Gemini TTS preview ids in your files and prints the replacement Google names for each one.
- SHA-256 the batch body into the Sume idempotency key for bulk chunks
Derive each bulk chunk's Idempotency-Key from a hash of its items so replays reuse the queue and edited rows get a new key instead of a 409 conflict.
- Turn a shot list into Gemini Omni requests: Python on Sume
Submit every line of a shot list as its own Omni job with stable Idempotency-Keys, then poll them in one pass. Runnable Python and the cost of a 3-shot list.
Written by Sume