Seedance 2.5 from ByteDance is officially available on Apiframe.

How to Build a Text-to-Video API Integration: A Python Tutorial (2026)

A hands-on Python tutorial for calling a text-to-video API: auth, requests, polling, and code.

Janice Last updated September 3, 2026 July 8, 2026 · 9 min read Intermediate
How to Build a Text-to-Video API Integration: A Python Tutorial (2026)

Text-to-video generation is now reliable enough to be a real building block for production products, not just a proof-of-concept demo. This tutorial walks through building a working Python integration against a text-to-video API from scratch: authentication, sending your first request, checking on the result, downloading the output, and handling the errors that will inevitably show up. By the end, you will have a small FastAPI service that wraps the whole flow. If you want the concepts behind the technology first, our explainer on how text-to-video generation works is a good primer before diving into code.

What You'll Build

A minimal backend service that accepts a text prompt, starts a video generation job, and lets a client check on progress until the video is ready to download.

Prerequisites:

  • Python 3.9 or later
  • An API key from Apiframe (see the getting started guide) (the examples below use Apiframe's unified video API, but the same pattern applies to most async generation APIs)
  • The requests package (pip install requests), and fastapi plus uvicorn for the final example (pip install fastapi uvicorn)

How Text-to-Video APIs Differ from Image-to-Video

If you have already built an image-to-video integration, most of the setup here will look familiar, but a few real differences are worth calling out before you write any code.

Prompt-only input instead of a reference image. Image-to-video generation starts from a still image and animates it. Text-to-video has no visual anchor at all: the model has to invent the scene, the subject, and the motion purely from the words in your prompt. That makes prompt quality matter more. Vague prompts tend to produce vague, drifting motion.

Duration, motion, and camera control. Text-to-video models typically let you set the clip length (often 5, 6, or 10 seconds depending on the model), and some support basic camera direction inside the prompt itself, like a slow zoom or a pan, since there is no reference frame to anchor the camera to.

If you later want to add image-to-video to the same product, for example letting users upload a product photo and animate it, most APIs support both modes through the same endpoint, just with an added image parameter. That means the two features can share almost all of the code in this tutorial.

Step 1: Authentication and Setup

Every request needs your API key passed in an X-API-Key header. Keep it out of source control, load it from an environment variable:

python
import os
import requests

API_KEY = os.environ["APIFRAME_API_KEY"]
BASE_URL = "https://api.apiframe.ai/v2"

HEADERS = {
    "X-API-Key": API_KEY,
    "Content-Type": "application/json",
}

Store the key in a .env file or your platform's secrets manager, never hardcode it, and never commit it.

Step 2: Sending Your First Text-to-Video Request

Video generation runs as a background process. You submit a prompt, and the API responds right away with a job ID and a QUEUED status, rather than making you wait for the finished video.

Here is a request using Hailuo 02, a text-to-video model that supports 6 or 10 second clips at 768p or 1080p resolution.

python
def submit_video_job(prompt: str, duration: int = 6, resolution: str = "768p") -> dict:
    payload = {
        "prompt": prompt,
        "model": "hailuo-02",
        "hailuoParams": {
            "duration": duration,
            "resolution": resolution,
        },
    }
    response = requests.post(
        f"{BASE_URL}/videos/generate",
        headers=HEADERS,
        json=payload,
    )
    response.raise_for_status()
    return response.json()


job = submit_video_job("a paper airplane gliding through a sunlit office, slow motion")
print(job)  # {"jobId": "b2c3d4e5-...", "status": "QUEUED"}

Note the constraint: 1080p generations only support the 6 second duration, not 10, so validate that combination before you submit it if you are exposing these options to end users. This limit applies across the whole Hailuo lineup, so it is worth keeping in mind even if you later switch models. Our Hailuo versions comparison breaks down which version fits which use case, and Apiframe has since added the newer Hailuo 03 model if you want to compare against the latest release.

The prompt field accepts up to 4,000 characters, and model is required on every request. Other text-to-video models (Kling, Seedance, Wan) take the same top-level shape but their own model-specific parameters object, for example klingParams or seedanceParams, so check the model's own reference page for the exact field names before swapping models.

Step 3: Polling for Completion

Once you have a job ID, poll the jobs endpoint until the status changes to COMPLETED or FAILED. Video jobs take longer than image jobs, so poll every 5 to 10 seconds rather than every second or two:

python
import time

def wait_for_completion(job_id: str, poll_interval: int = 6, timeout: int = 600) -> dict:
    elapsed = 0
    while elapsed < timeout:
        response = requests.get(f"{BASE_URL}/jobs/{job_id}", headers=HEADERS)
        response.raise_for_status()
        job = response.json()

        if job["status"] == "COMPLETED":
            return job
        if job["status"] == "FAILED":
            raise RuntimeError(f"Generation failed: {job.get('error')}")

        time.sleep(poll_interval)
        elapsed += poll_interval

    raise TimeoutError(f"Job {job_id} did not complete within {timeout} seconds")


result = wait_for_completion(job["jobId"])
print(result["result"])  # {"videoUrl": "https://cdn2.apiframe.ai/videos/....mp4"}

A note on webhooks. Polling works fine for scripts, background jobs, or low-volume use, but if you're running this at any real scale, a webhook is the better fit. Our step-by-step webhook setup guide walks through securing and testing your endpoint. webhookUrl and webhookEvents in the original request (["completed", "failed"]) and the API will POST the result to your server the moment the job finishes, instead of your code repeatedly checking in on it. Apiframe signs webhook payloads with an HMAC signature so you can verify they actually came from the API before trusting the payload.

Step 4: Handling and Storing the Output Video

Once the job is COMPLETED, the result contains a hosted video URL. Download and validate it before handing it off to the rest of your app:

python
def download_video(video_url: str, output_path: str) -> str:
    response = requests.get(video_url, stream=True, timeout=60)
    response.raise_for_status()

    content_type = response.headers.get("Content-Type", "")
    if "video" not in content_type:
        raise ValueError(f"Unexpected content type: {content_type}")

    with open(output_path, "wb") as f:
        for chunk in response.iter_content(chunk_size=8192):
            f.write(chunk)

    return output_path


video_url = result["result"]["videoUrl"]
download_video(video_url, "output.mp4")

Generated files are stored on the provider's servers for a limited window (Apiframe keeps results for 90 days), so if your product needs to keep the video long-term, download it and move it to your own storage rather than relying on that link indefinitely. If you want to plan your storage and compute budget ahead of time, our guide on estimating AI video generation costs breaks down the numbers.

Step 5: Error Handling and Retries

A few failure modes show up often enough that it's worth handling them explicitly rather than letting an unhandled exception take down the request:

python
def submit_with_retry(prompt: str, max_retries: int = 3, **kwargs) -> dict:
    for attempt in range(1, max_retries + 1):
        try:
            response = requests.post(
                f"{BASE_URL}/videos/generate",
                headers=HEADERS,
                json={"prompt": prompt, "model": "hailuo-02", "hailuoParams": kwargs},
                timeout=30,
            )
            if response.status_code == 429:
                wait = 2 ** attempt
                time.sleep(wait)
                continue
            response.raise_for_status()
            return response.json()
        except requests.exceptions.Timeout:
            if attempt == max_retries:
                raise
            time.sleep(2 ** attempt)

    raise RuntimeError("Max retries exceeded")

Common failure modes to plan for:

  • Timeouts: the request itself, not the generation, timing out on a slow connection. Set a reasonable timeout on your HTTP client and wait and retry.
  • Content moderation rejections: a FAILED job with an error like "Provider returned an error: content policy violation". These won't succeed on retry with the same prompt, so surface the error to the user instead of silently retrying.
  • Malformed prompts or parameters: a 400 response with a details object describing which field failed validation. Worth logging in full during development.
  • Rate limits: a 429 response means you have exceeded your per-minute request limit. Back off and retry rather than hammering the endpoint.
  • Insufficient credits: a 402 response. Since credits are deducted at submission and refunded automatically on failure, this only happens when your balance is genuinely too low to cover the job.

Putting It Together: A Minimal FastAPI Endpoint

Here are all five steps combined into a small service with two endpoints: one to start a generation, one to check on it.

python
import os
import time
import requests
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

API_KEY = os.environ["APIFRAME_API_KEY"]
BASE_URL = "https://api.apiframe.ai/v2"
HEADERS = {"X-API-Key": API_KEY, "Content-Type": "application/json"}

app = FastAPI()


class GenerateRequest(BaseModel):
    prompt: str
    duration: int = 6
    resolution: str = "768p"


@app.post("/generate")
def generate_video(req: GenerateRequest):
    if req.resolution == "1080p" and req.duration == 10:
        raise HTTPException(400, "1080p does not support 10 second duration")

    payload = {
        "prompt": req.prompt,
        "model": "hailuo-02",
        "hailuoParams": {"duration": req.duration, "resolution": req.resolution},
    }
    response = requests.post(f"{BASE_URL}/videos/generate", headers=HEADERS, json=payload)
    if response.status_code == 402:
        raise HTTPException(402, "Insufficient credits")
    response.raise_for_status()
    return response.json()


@app.get("/status/{job_id}")
def get_status(job_id: str):
    response = requests.get(f"{BASE_URL}/jobs/{job_id}", headers=HEADERS)
    if response.status_code == 404:
        raise HTTPException(404, "Job not found")
    response.raise_for_status()
    return response.json()

Run it with uvicorn main:app --reload, POST a prompt to /generate, then poll /status/{job_id} from your frontend until the status is COMPLETED.

Next Steps: Choosing a Model for Text-to-Video

Hailuo 02 is a solid default to start with, but it is one of several text-to-video models available through the same endpoint pattern, each with its own tradeoffs. Our full AI video API guide covers how these models compare beyond just Hailuo.

  • Kling: strong general-purpose quality, with newer versions (2.6 and up) supporting synchronized audio generation alongside the video.
  • Seedance: a wide range of speed and resolution tiers, useful if you need to dial cost up or down depending on the use case.
  • Hailuo: good realistic motion at a moderate price point, the one used throughout this tutorial.
  • Wan: supports longer clips and multi-shot storytelling in its newer versions, worth a look if you need more than a single short clip.

Since all of these sit behind the same POST /v2/videos/generate endpoint with the same job and webhook pattern, swapping models in the code above is mostly a matter of changing the model field and its matching parameters object, not rebuilding the integration.

Adding a reference image later, to support image-to-video alongside text-to-video, generally just means adding an image field inside the model's parameters object. Worth planning for if you expect to add that mode down the line.

FAQ

How long does generation take?

It varies by model and settings, but video jobs commonly take anywhere from 30 seconds to a few minutes. Checking every 5 to 10 seconds, or using a webhook, is the recommended approach rather than assuming a fixed wait time.

Can I convert this to image-to-video later?

Yes. Most text-to-video models on a unified API also support image-to-video through the same endpoint, typically by adding an image URL parameter. The authentication, job checking, and download code in this tutorial stays the same either way.

What happens if my prompt gets rejected?

You will get a FAILED job status with an error message describing the rejection, most commonly a content policy violation. Credits spent on a failed job are typically refunded automatically, so retrying with a revised prompt does not cost you double.

Do I need to handle video storage myself?

For anything you want to keep beyond the provider's retention window (90 days on Apiframe), yes. Download the file and store it in your own infrastructure rather than relying on the hosted link long-term.

Power your next AI product with Apiframe.

Instant access to 70+ media models through a single API. Start free and scale when you're ready.