Skip to main content

manimotion

The official Python SDK for manimotion: send a topic, get back a narrated video lecture.

from manimotion import Manimotion

client = Manimotion()  # reads MANIMOTION_API_KEY

video = client.videos.generate("Explain how binary search works")
print(video.video_url)  # https://…/lecture.mp4
  • Python 3.9+
  • Sync and async clients
  • Fully type-hinted
  • Built-in polling, progress callbacks, retries and typed errors

Contents


Install

pip install manimotion
# or
uv add manimotion
poetry add manimotion

Get an API key

  1. Sign in at manimotion.dev.
  2. Open Settings → API keys and create a key. It starts with chalk_.
  3. Put it in an environment variable. Never hard-code it or commit it to git.
export MANIMOTION_API_KEY=chalk_xxxxxxxxxxxxxxxx

Quickstart

from manimotion import Manimotion

client = Manimotion()

video = client.videos.generate(
    "Why is the sky blue? Explain Rayleigh scattering for high-school students.",
    on_progress=lambda v: print(f"[{v.status}] {v.message or ''}"),
)

print("Done:", video.video_url)

generate() creates the video and waits until it's ready. Most videos take 1–5 minutes.

Storage: where your video goes

manimotion uploads the finished MP4 to your bucket, so you own the file and the URL. There are three ways to set this up:

1. Save a bucket once (easiest). Add it in Settings → Storage. Once it's saved, it's used automatically and you don't need to pass anything.

2. Use a specific saved bucket. Click Copy id next to it in Settings → Storage:

client.videos.generate(
    "Intro to photosynthesis",
    storage={"integration_id": "your-integration-id"},
)

3. Pass credentials for this request only:

import os

# Cloudflare R2
client.videos.generate(
    "Intro to photosynthesis",
    storage={
        "inline": {
            "provider": "r2",
            "bucket": "my-videos",
            "account_id": os.environ["R2_ACCOUNT_ID"],
            "access_key_id": os.environ["R2_ACCESS_KEY_ID"],
            "secret_access_key": os.environ["R2_SECRET_ACCESS_KEY"],
            "public_url": "https://pub-xxxx.r2.dev",
        }
    },
)

# AWS S3 (or "minio" / "backblaze" / "custom_s3" with an "endpoint")
client.videos.generate(
    "Intro to photosynthesis",
    storage={
        "inline": {
            "provider": "s3",
            "bucket": "my-videos",
            "region": "us-east-1",
            "access_key_id": os.environ["AWS_ACCESS_KEY_ID"],
            "secret_access_key": os.environ["AWS_SECRET_ACCESS_KEY"],
        }
    },
)

If none of these is set up, the call raises BadRequestError saying "No storage configured". See the storage guide.

Creating videos

Argument Type Default What it does
prompt str required What to teach (10–2000 characters).
style "auto", "math", "graphics" "auto" math for equations and graphs; graphics for concepts, processes and comparisons.
duration int (15–180) automatic Soft target length in seconds.
model str default Model that writes the lesson, e.g. google/gemini-2.5-flash.
tier "tier1", "tier2", "tier3" automatic tier1 fastest, tier2 standard, tier3 most detailed.
storage dict, see Storage your saved bucket Where to upload.

Fire-and-forget (web apps, queues)

generate() blocks for minutes. In a web request, start the job and check on it later instead:

job = client.videos.create("The water cycle")
save_to_db(job.id)

# later…
video = client.videos.retrieve(job.id)
if video.status == "completed":
    print(video.video_url)
elif video.status == "failed":
    print(video.error)

Waiting and progress

video = client.videos.wait(
    job.id,
    poll_interval=5,        # seconds, default 3
    timeout=10 * 60,        # seconds, default 20 min
    on_progress=lambda v: print(v.status, v.message, v.eta_seconds),
)

Statuses, in order: queued → planning → generating_audio → generating_code → merging → uploading → completed (or failed).

If wait() times out, the video keeps rendering on the server, so you can retrieve() it later.

The Video object

Video(
    id="a1b2c3…",
    status="completed",
    video_url="https://…/lecture.mp4",
    error=None,            # short reason when status == "failed"
    cached=False,          # True if an identical recent request was reused
    style="graphics",      # "math" | "graphics" | None
    duration=94.2,         # seconds, once completed
    phase="completed",
    message="Done!",
    eta_seconds=0,
    eta_display="~2–3 min",
    tier="tier2",
)

video.done   # True once completed or failed
video.raw    # the untouched JSON from the API

Async usage

Every method has an async twin on AsyncManimotion:

import asyncio
from manimotion import AsyncManimotion

async def main():
    async with AsyncManimotion() as client:
        video = await client.videos.generate("Explain recursion with a factorial example")
        print(video.video_url)

asyncio.run(main())

on_progress can be a normal function or an async def.

Errors

Every error inherits from ManimotionError. Errors from the API also have .status_code, .message and .body.

from manimotion import (
    Manimotion, AuthenticationError, RateLimitError, VideoFailedError,
)

try:
    video = client.videos.generate("Explain TCP handshakes")
except AuthenticationError:
    ...  # wrong or revoked key
except RateLimitError as e:
    ...  # too many requests, or plan quota used up (see e.message)
except VideoFailedError as e:
    print("Video failed:", e, e.video.id)
Error When
BadRequestError 400: e.g. no storage configured, bad storage credentials.
AuthenticationError 401: missing, wrong or revoked API key.
NotFoundError 404: unknown job id (or it belongs to another key).
ValidationError 422: a parameter is invalid (message says which).
RateLimitError 429: too many requests, or daily/monthly quota reached.
InternalServerError 5xx: something broke on our side.
APIConnectionError Couldn't reach the API (offline, DNS, …).
APITimeoutError A request or wait() took too long.
VideoFailedError The job ended with status == "failed".

Retries: status checks (retrieve, wait, health) are retried automatically on network errors and 5xx. create() is never retried automatically, because a retry could start a second video and use up your quota.

Client options

client = Manimotion(
    api_key="chalk_…",                     # default: MANIMOTION_API_KEY env var
    base_url="https://api.manimotion.dev",  # default: MANIMOTION_BASE_URL or this
    timeout=60.0,                           # seconds per HTTP request
    max_retries=2,                          # status checks only
    default_headers={"x-request-source": "my-app"},
    http_client=None,                       # bring your own httpx.Client (proxies, etc.)
)

Use with Manimotion() as client: (or call client.close()) to close connections when you're done.

Framework examples

FastAPI

from fastapi import FastAPI, HTTPException
from manimotion import AsyncManimotion, APIError

app = FastAPI()
client = AsyncManimotion()

@app.post("/lectures")
async def start_lecture(prompt: str):
    try:
        job = await client.videos.create(prompt)
    except APIError as e:
        raise HTTPException(e.status_code, e.message)
    return {"id": job.id, "eta": job.eta_display}

@app.get("/lectures/{video_id}")
async def lecture_status(video_id: str):
    video = await client.videos.retrieve(video_id)
    return {"status": video.status, "url": video.video_url, "message": video.message}

Django / Flask

Use the sync Manimotion client. Call create() in the view, store job.id, and run wait() in a background worker (Celery, RQ, a cron job) so web requests stay fast.

Jupyter notebook

from IPython.display import Video as Player
from manimotion import Manimotion

video = Manimotion().videos.generate("Visualize the derivative of x^2")
Player(video.video_url, width=720)

API reference

Method Returns Description
videos.create(prompt, **options) Video Start a video; returns right away.
videos.retrieve(video_id) Video Latest status of a job.
videos.wait(video_id, **wait_options) Video Poll until completed (raises if failed).
videos.generate(prompt, **options) Video create + wait in one call.
health() Health API status; no key needed.

The HTTP API behind these methods is documented at manimotion.dev/docs/api.

FAQ

How long does a video take? Usually 1–5 minutes, depending on length and tier. video.eta_display gives an estimate.

I sent the same prompt twice and got the same video instantly. Identical requests made within a few hours are served from cache (video.cached is True). Change the prompt slightly to get a fresh render.

My job id stopped working after a while. Job status is kept for a limited time. Save video_url as soon as the job completes; the file in your bucket stays there.

License

Apache-2.0

Metadata

Release files for manimotion 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for manimotion 0.1.0
File Size Uploaded
manimotion-0.1.0.tar.gz 17.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for manimotion 0.1.0
File Interpreter ABI Platform
manimotion-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 34.2 kB

Release files / manimotion-0.1.0.tar.gz

Download URL manimotion-0.1.0.tar.gz
Size 17.2 kB
Tags Source
SHA-256 checksum
How to use checksums
6713a2ce15b553c37458828118b6db8172345a9566870bae62fee5e01804e734
BLAKE2b-256 checksum
How to use checksums
5157ecf9733839ffbd6042758882f1cc3792de2a8b019057d41e99cf6966052c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / manimotion-0.1.0-py3-none-any.whl

Download URL manimotion-0.1.0-py3-none-any.whl
Size 17.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f2f3f1e5cea96e476b90aa04196b81b274df98a939cc865ffaf21aa87681e3da
BLAKE2b-256 checksum
How to use checksums
f8b402f854905ba0e624bd76903846e78b3dc2e8a871b575cd59ab0d7bef6d05
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page