Back to Guides

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.

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

Text-to-video generation has gotten good enough that it's now a realistic building block for real products, not just a novelty demo. This tutorial walks through building a working Python integration against a text-to-video API from scratch: authentication, sending your first request, polling for the result, downloading the output, and handling the errors that will inevitably show up. By the end you'll have a small FastAPI service that wraps the whole flow.

What You'll Build

A minimal backend service that accepts a text prompt, kicks off 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 (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've already built an image-to-video integration, most of the plumbing here will look familiar, but there are a few real differences worth calling out before you write any code.

Prompt-only input vs. 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 expose parameters for clip length (often 5, 6, or 10 seconds depending on the model), and some support basic camera direction in the prompt itself (a slow zoom, a pan) since there's no reference frame to anchor the camera to.

If you later want to add image-to-video to the same product (say, letting users upload a product photo and animate it), most APIs support both modes through the same endpoint, just with an added image parameter, so 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 is asynchronous. You submit a prompt, and the API responds immediately with a job ID and a QUEUED status rather than making you wait for the finished video.

Here's a request using Hailuo 02, a text-to-video model that supports 6 or 10 second clips at 768p or 1080p:

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 if you're exposing these options to end users.

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. Pass a 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 you burning requests checking in on it. Apiframe signs webhook payloads with an HMAC-SHA256 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 CDN 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 typically hosted on the provider's CDN 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 the CDN link indefinitely.

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 retry with backoff.
  • 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've exceeded the requests-per-minute cap. 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's all five steps combined into a small service with two endpoints: one to kick off 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's one of several text-to-video models available through the same endpoint pattern, each with its own tradeoffs:

  • 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. Polling 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 polling, and download code in this tutorial stays the same either way.

What happens if my prompt gets rejected?

You'll get a FAILED job status with an error message describing the rejection (commonly a content policy violation). Credits spent on a failed job are typically refunded automatically, so retrying with a revised prompt doesn't 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 CDN URL long-term.

Ready to start building?

Get your API key and start generating AI content in minutes.