> Markdown copy of https://makernice.com/docs/python/ for AI tools. The HTML page is the canonical version.

# Use the Makernice API with Python

Call Makernice from Python with the `requests` library: quote the exact price, create the generation, poll until it finishes, then download the file. Four calls, no SDK to install.

[Makernice Team](https://makernice.com/about/) · Updated November 11, 2026

## Before you start

-   Create an API key in the [console](https://makernice.com/console/keys/) and export it as `MAKERNICE_API_KEY`.
-   Python 3 and `pip install requests`.

## Complete example

quickstart.py

```
import os
import time
import uuid
from decimal import Decimal

import requests

API = "https://api.makernice.com/v1"
ORIGIN = "https://api.makernice.com"
HEADERS = {"Authorization": f"Bearer {os.environ['MAKERNICE_API_KEY']}"}
TIMEOUT_SECONDS = 30
POLL_INTERVAL_SECONDS = 5
MAX_WAIT_SECONDS = 30 * 60
FINAL = {"succeeded", "failed", "canceled"}

def call(method, path, headers=None, **kwargs):
    # Waits out 429 (rate_limited, concurrency_limit_reached) using Retry-After; raises on any other error.
    for _ in range(5):
        r = requests.request(method, API + path, headers={**HEADERS, **(headers or {})}, timeout=TIMEOUT_SECONDS, **kwargs)
        if r.status_code == 429:
            time.sleep(int(r.headers.get("Retry-After", "5")))
            continue
        if not r.ok:
            raise RuntimeError(f"{method} {path} -> {r.status_code}: {r.text}")
        return r.json()
    raise RuntimeError(f"{method} {path}: still rate limited")

job = {
    "model": "seedance-2.5",
    "mode": "t2v",
    "prompt": "A slow dolly shot through a greenhouse",
    "duration": 5,
    "resolution": "720p",
}

# 1. Quote: the exact amount, locked for 12 minutes
quote = call("POST", "/generations/quote", json=job)
usd = Decimal(quote["amount"]).scaleb(-6)  # micro-USD integer string -> dollars, no float rounding
print(f"Quoted ${usd} (price list v{quote['price_version']}), valid until {quote['expires_at']}")

# 2. Create: same input + quote_id. A retry with the same Idempotency-Key returns the same generation
idempotency_key = str(uuid.uuid4())
gen = call("POST", "/generations", json={"quote_id": quote["id"], "input": job}, headers={"Idempotency-Key": idempotency_key})

# 3. Poll: queued -> generating -> moderating -> succeeded | failed | canceled (checking: keep polling)
deadline = time.monotonic() + MAX_WAIT_SECONDS
while gen["status"] not in FINAL:
    if time.monotonic() > deadline:
        raise SystemExit(f"{gen['id']} is still {gen['status']}; poll it again later")
    time.sleep(POLL_INTERVAL_SECONDS)
    gen = call("GET", f"/generations/{gen['id']}")

if gen["status"] != "succeeded":
    # billing is "released": the hold went back to your balance
    raise SystemExit(f"{gen['status']}, billing {gen['billing']}: {gen['error']}")

# 4. Download: outputs[].url is relative to the API origin and redirects (302) to the file
output = gen["outputs"][0]
if output["state"] == "blocked":
    raise SystemExit("Output blocked by moderation (the run is charged); contact support for a manual review")
r = requests.get(ORIGIN + output["url"], headers=HEADERS, timeout=120)  # requests drops Authorization on the redirect
r.raise_for_status()
with open("output.mp4", "wb") as f:
    f.write(r.content)
print(f"Saved output.mp4, charged ${Decimal(gen['amount']).scaleb(-6)}")
```

## What the code does

1.  **Quote**: `POST /v1/generations/quote` returns the exact amount as an integer string in micro-USD, locked for 12 minutes. Nothing is charged yet.
2.  **Create**: `POST /v1/generations` with the `quote_id`, the same input and an `Idempotency-Key` header. Retrying with the same key returns the same generation instead of a second one.
3.  **Wait**: poll `GET /v1/generations/{id}` until it is `succeeded`, `failed` or `canceled`, or pass a `callback_url` and we POST the result to your server.
4.  **Download**: each `outputs[].url` answers with a 302 to a signed file URL.

## Python notes

-   Amounts are integer strings in micro-USD. The example converts them with `Decimal`, never `float`, so nothing is rounded.
-   A 429 response carries `Retry-After`. The `call()` helper waits it out and retries up to 5 times.
-   `requests` drops the `Authorization` header when it follows the cross-host download redirect. That is what you want: the signed file URL does not need your key.

## FAQ

### Is there an official SDK?

No. The REST API is four calls and the code on this page is a complete client. If you want a generated client, the OpenAPI description is at [/openapi.json](https://makernice.com/openapi.json).

### Should I poll or use webhooks?

Polling is fine for scripts and batch jobs. For a web app, pass a `callback_url` and verify the signature; the [FastAPI guide](https://makernice.com/docs/fastapi/) shows a complete receiver, and the same `verify_webhook` function works in Django (`request.body`) and Flask (`request.get_data()`).

### Do I pay if a generation fails?

No. The amount is held when you create the generation and released in full if it fails or is canceled. An output blocked by content moderation after the run is still charged. Details: [No charge for failed generations](https://makernice.com/pricing/failed-generations/).

## Related

-   [API docs: Quickstart](https://makernice.com/docs/#quickstart)
-   [API docs: Webhooks](https://makernice.com/docs/#webhooks)
-   [API docs: Errors](https://makernice.com/docs/#errors)
-   [Use the Makernice API with Node.js](https://makernice.com/docs/node/)
-   [Use the Makernice API with Next.js](https://makernice.com/docs/nextjs/)
-   [Use the Makernice API with FastAPI](https://makernice.com/docs/fastapi/)
-   [Use it from Claude Code, Cursor and other AI tools (MCP)](https://makernice.com/mcp/)
