Render a video with webhooks
Start a render, have it call your server when it is done, and download the video, without polling.
A video takes from about twenty seconds to a few minutes to render. Rather than poll for it, give the job a callback_url and let it tell you when it is done.
1. Get your webhook secret
Deliveries are signed with your webhook secret, so your server can tell they came from us. Fetch it once and give it to the server that will receive them; it is not your API key, and that server needs no key.
# Your webhook signing secret (whsec_...), made on the first call
curl https://api.vultrinference.com/v1/webhooks/secret \
-H "Authorization: Bearer $VULTR_INFERENCE_API_KEY"2. Run a receiver
The receiver checks the signature, answers within 10 seconds, ignores a delivery it has already handled, and then does the work. It must be reachable at a public https address.
import hashlib
import hmac
import json
import os
import time
from http.server import BaseHTTPRequestHandler, HTTPServer
SECRET = os.environ["VULTR_WEBHOOK_SECRET"] # whsec_..., from GET /v1/webhooks/secret
handled = set() # keep these in your database: a delivery can arrive more than once
def verified(header: str, body: bytes) -> bool:
pairs = [part.split("=", 1) for part in header.split(",")]
timestamp = next((value for key, value in pairs if key == "t"), "0")
if abs(time.time() - int(timestamp)) > 300:
return False
expected = hmac.new(SECRET.encode(), f"{timestamp}.".encode() + body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected, value) for key, value in pairs if key == "v1")
class Hook(BaseHTTPRequestHandler):
def do_POST(self):
body = self.rfile.read(int(self.headers["Content-Length"]))
if not verified(self.headers.get("X-Vultr-Signature", ""), body):
self.send_response(400)
self.end_headers()
return
# Answer within 10 seconds, or the delivery is sent again: do slow work after this
self.send_response(204)
self.end_headers()
key = self.headers["X-Vultr-Idempotency-Key"]
if key in handled:
return
handled.add(key)
event = json.loads(body)
job = event["data"]
if event["type"].endswith(".completed"):
print(job["id"], "is ready:", job["unsigned_urls"][0])
else:
print(job["id"], "ended", job["status"], job.get("error"))
if __name__ == "__main__":
HTTPServer(("", int(os.environ.get("PORT", "8000"))), Hook).serve_forever()Webhooks has the events, the headers, the retries and how to rotate the secret.
3. Start the render
POST /v1/videos checks the request and answers 202 at once with the job, pending. A field the model does not take is a 400 now, never a job that fails later.
# The job answers 202 at once; your callback_url gets the result when it is done
curl https://api.vultrinference.com/v1/videos \
-H "Authorization: Bearer $VULTR_INFERENCE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "minimax-h3",
"prompt": "A paper boat drifting down a rain-soaked street at dusk",
"duration": 4,
"callback_url": "https://example.com/hooks/video"
}'import os
from openai import OpenAI
client = OpenAI(base_url="https://api.vultrinference.com/v1", api_key=os.environ["VULTR_INFERENCE_API_KEY"])
# The job answers at once; your callback_url gets the result when it is done
video = client.videos.create(
model="minimax-h3",
prompt="A paper boat drifting down a rain-soaked street at dusk",
seconds="4",
extra_body={"callback_url": "https://example.com/hooks/video"},
)
print(video.id, video.status)What minimax-h3 takes, in short; its page has everything:
| Field | Takes |
|---|---|
duration |
4 to 15 seconds, 6 by default |
aspect_ratio |
16:9, 9:16, 4:3, 3:4, 1:1 or 21:9 |
generate_audio |
false for a silent video; audio is made by default |
A job with only a prompt renders in about three seconds per second of video.
4. Handle the result
Your receiver gets one of four events:
| Event | Do |
|---|---|
video. |
Download the video (below). data.usage has what it cost. |
video. |
Read data.error, and decide by data.error_code (below). Not billed. |
video. |
Nothing: the job was deleted before it finished. Not billed. |
video. |
Nothing: the video was deleted 7 days after it completed. |
error_code |
Means | Do |
|---|---|---|
provider_error |
The render failed or was interrupted on our side | Send the same request again |
timeout |
The render took too long | Send it again |
invalid_request |
An input could not be used, such as a media URL that would not download | Fix the request first |
payload_too_large |
A media input was over its size limit | Fix the request first |
server_error |
The job could not run, for example because its model was removed | Check the model, then send it again |
5. Download it
GET /v1/videos/{id}/content returns the MP4 itself; the job's unsigned_urls point there too. It needs your API key, and works until expires_at, 7 days after the job completed.
# The video, with your API key, until the job's expires_at (7 days after it completed)
curl -o video.mp4 "https://api.vultrinference.com/v1/videos/$VIDEO_ID/content?index=0" \
-H "Authorization: Bearer $VULTR_INFERENCE_API_KEY"Billing
A job is billed only when it completes: the duration you asked for, times the model's price per second. A higher quality bills more seconds (standard 3x, max 5x), as does a raw num_inference_steps above the fast default; usage.billed_seconds shows the result. Failed and cancelled jobs cost nothing.
Endpoints