Docs
Get an API key
Workflows›Render a video with webhooks

Render a video with webhooks

Start a render, have it call your server when it is done, and download the video, without polling.

View .md

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 secret
# 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.

A receiver
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.

Create the job
# 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"
  }'

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.generation.completed Download the video (below). data.usage has what it cost.
video.generation.failed Read data.error, and decide by data.error_code (below). Not billed.
video.generation.cancelled Nothing: the job was deleted before it finished. Not billed.
video.generation.expired 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.

Download the video
# 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.