Seedance 2.5 API Tutorial: Create Videos With People in Python
Build a Seedance 2.5 video workflow with Affogato’s API: upload a person’s photo, estimate credits, submit, poll and download with Python.

A video workflow built around a person can stop at the first upload if your provider blocks photos containing human faces. Affogato supports real-person photos in its Seedance 2.5 workflow, so your application can start with the person your story actually needs.
Provider restrictions differ: a model name alone does not tell you whether an endpoint accepts your input. If another service rejects a photo because it contains a real person, Affogato offers a supported route for creating videos with people. That does not remove consent requirements, content safeguards or the model’s file limits.
This tutorial follows the Python workflow: choose a model, upload a photo, estimate credits, submit once, follow the job and save its output.
Start with the person, then describe a new scene
Let’s use Noor, a fictional character. These photos show her in two everyday settings. In your own application, you can use an authorized real-person photo; Noor gives us a repeatable character for this walkthrough.


The goal is a new performance, not simply a moving photograph. In this API example, we upload only the street photo and ask for a six-second black-tie reception scene. Describe a place, an outfit and one action. Our Seedance 2.5 people guide shows completed examples in three settings; those clips were made separately from this Python tutorial.
The complete Python run succeeded and downloaded output.mp4. This is one measured run, not a promise about every job’s speed or cost.
Open the example video · Download the complete Python example
1. Set up your API key
Open the Developers page in Affogato and create a key for the workspace whose credits you want to use. Keep the key in an environment variable named AFFOGATO_API_KEY, and read it from your Python process. Do not place it in a page’s JavaScript, a notebook you share publicly or a committed configuration file.
The public API base is https://api.affogato.ai/v1. Requests authenticate with an Authorization: Bearer … header. The web app’s internal endpoints are a different interface; use the documented developer API for your integration.
Before starting, make sure your account is eligible for API access and has enough credits for the generation. Creating a key and funding a generation are separate steps.
python -m pip install requests
# Replace the placeholder with your API key on your server.
export AFFOGATO_API_KEY="YOUR_API_KEY"
# The example below never prints or stores the key.2. Find the right Seedance 2.5 model
Request the model catalog and find the Seedance 2.5 option that supports the kind of video you want to make. Then retrieve that model’s details. Its input slots, parameters and example request are the contract your code should follow.
This matters because a first-frame video and a video built around a character are different requests. A field that works for one model should not be assumed to work for another. Use the returned model ID exactly, and check the supported duration, aspect ratio and resolution before constructing your input.
For an input photo, upload the file with the public files endpoint and place its returned file ID in the appropriate model input slot. Keep this preparation separate from the paid generation so you can inspect the request before submitting it.
The individual HTTP requests below use cURL. The downloadable Python script performs this entire loop in one run. We verified the live model ID seedance-2-5-ref2v-modelark and its single-file reference_image input. The optional video input is omitted.
curl --fail-with-body \
"https://api.affogato.ai/v1/models/seedance-2-5-ref2v-modelark" \
-H "Authorization: Bearer $AFFOGATO_API_KEY"3. Upload your photos
Use the public files endpoint to upload the person’s photo. Put its returned file ID in the model’s image input. Upload each photo separately; combine IDs only if the input explicitly has multiple=true.
curl --fail-with-body \
"https://api.affogato.ai/v1/files" \
-H "Authorization: Bearer $AFFOGATO_API_KEY" \
-F "file=@noor-street.webp;type=image/webp" \
-o uploaded-file.jsonOur upload returned the following fields (excerpt). Use the ID from your own upload:
{
"id": "e5e7368f-7c43-4860-a337-e6c64a43b6df",
"mime_type": "image/webp"
}Save the full request body below as request.json and replace YOUR_UPLOADED_FILE_ID. It uses the exact prompt and settings from our test: six seconds, 720p, 9:16 and native audio.
Full request body and exact prompt
{
"model": "seedance-2-5-ref2v-modelark",
"input": {
"reference_image": "YOUR_UPLOADED_FILE_ID",
"prompt": "Create a new six-second video starring Noor, the adult woman in #Image1. Use the photo only for her identity: preserve her face, curly updo, loose face-framing curls and hoop earrings. Compose a new scene, not the street photograph. Noor wears a floor-length satin evening gown at a black-tie reception. She walks toward the camera, pauses beside a tall floral arrangement and smiles at someone just beside the camera. Guests in tuxedos and evening dresses converse behind her. One continuous shot, camera moving gently backward, her face clearly visible. Quiet reception ambience, no distinct dialogue, music, text or overlays.",
"duration": 6,
"resolution": "720p",
"aspect_ratio": "9:16",
"generate_audio": true
}
}You can also download the request template.
Edit request.json before generating: adapt the example prompt, fill in the correct file input and choose supported settings from the model response. Affogato’s support for real-person photos opens up the creative workflow; matching the schema makes the request valid.
4. Estimate the request before generating
Send your proposed model and input to POST /v1/generations/estimate. This gives you a credit estimate without creating the video. Show that amount in your application before the user commits to the generation.
Estimate the actual request you intend to submit. Changing its duration or another priced setting after estimating can change the cost. Keep the estimate with the request so you can later compare it with the completed generation’s recorded charge.
Download the Python runner and the Noor input photo into the same folder. The script looks up the model, uploads the photo, prepares the request, estimates, submits, polls and downloads. Its default run estimates only; --submit creates a paid job.
# Upload the photo and estimate first; no generation is created.
python complete_loop.py --photo noor-street.webp
# Generate, poll, and download output.mp4.
python complete_loop.py --photo noor-street.webp --submit --max-credits 1000
# If interrupted, run the same command again with the same state file.
# It resumes the existing job rather than creating another one.If you prefer separate requests, estimate the same request.json without creating a generation:
curl --fail-with-body \
"https://api.affogato.ai/v1/generations/estimate" \
-H "Authorization: Bearer $AFFOGATO_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @request.jsonActual estimate response, shortened to key fields:
{
"object": "estimate",
"model": "seedance-2-5-ref2v-modelark",
"credits": 515
}The 515-credit estimate applies to this exact request on November 10, 2026, not every Seedance video.
The 1,000-credit ceiling is a client-side example limit, not a quoted Seedance price. The runner stops if the estimate exceeds it and records the final charge in the saved response.
Submit once and save the generation ID
# Generate this value once per new job; keep it for retries.
JOB_KEY="$(python -c 'import uuid; print(uuid.uuid4())')"
curl --fail-with-body \
"https://api.affogato.ai/v1/generations" \
-H "Authorization: Bearer $AFFOGATO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $JOB_KEY" \
--data-binary @request.json \
-o generation.jsonActual create response (excerpt):
{
"id": "b89d784d-97fc-46ff-a38c-240caad08cd4",
"status": "queued",
"model": "seedance-2-5-ref2v-modelark",
"outputs": [],
"credits_charged": 515
}Keep JOB_KEY for retries. Do not rerun the line that generates a fresh key after an ambiguous timeout. The Python script saves its key and request before submitting.
Create the job with POST /v1/generations. Save the returned generation ID immediately: it is how your application follows the work after the initial connection ends.
Send a stable Idempotency-Key for this logical request. If the connection drops and you do not know whether the server accepted the submission, retry the same request with the same key. A fresh key would describe new work and could create another paid generation.
Keep the request body and idempotency key together. Do not reuse the key for an edited prompt or different settings.
5. Follow the job and download the video
Replace YOUR_GENERATION_ID with the ID from your create response:
GENERATION_ID="YOUR_GENERATION_ID"
curl --fail-with-body \
"https://api.affogato.ai/v1/generations/$GENERATION_ID?wait=25" \
-H "Authorization: Bearer $AFFOGATO_API_KEY" \
-o result.jsonRead GET /v1/generations/{id} to follow progress. The public API reports queued and processing while the job is in progress, then succeeded, failed or cancelled when it reaches a terminal state.
A Python timeout only tells you that your process stopped waiting. It does not establish that the generation failed or was cancelled. Retain the ID and resume checking the existing job rather than submitting another one.
On success, inspect the returned outputs and download the ready video from its output URL. Send your API credential only to Affogato’s API, not to the output download host. Save the original output, then make smaller delivery copies if your application needs them.
The runner saves api-example-state.json so you can resume the same job. On success it downloads the first ready video to output.mp4. Keep that state file with the unchanged request when retrying.
When result.json says succeeded, select a ready video output and download its URL without forwarding the API key:
Our actual completed response is shown below. It returned a ready MP4; only the output URL is replaced with a placeholder. The measured file contains 720 × 1280 video and an audio track.
{
"id": "b89d784d-97fc-46ff-a38c-240caad08cd4",
"status": "succeeded",
"model": "seedance-2-5-ref2v-modelark",
"credits_charged": 515,
"credits_refunded": 0,
"created_at": "2026-11-10T00:27:44.086720+00:00",
"completed_at": "2026-11-10T00:30:12.935173+00:00",
"outputs": [
{
"id": "a660b8b9-6f0f-488f-b0d6-662ca7f101af",
"status": "ready",
"mime_type": "video/mp4",
"duration_ms": 6080,
"url": "<output download URL from your response>"
}
]
}# Run only after result.json says succeeded and outputs contain a ready video.
python - <<'PYTHON'
import json, requests
from pathlib import Path
job = json.loads(Path("result.json").read_text())
assert job["status"] == "succeeded", job["status"]
video = next(o for o in job["outputs"]
if o["status"] == "ready" and o.get("url")
and (o.get("mime_type") or "").startswith("video/"))
# No API Authorization header is sent to the download URL.
r = requests.get(video["url"], timeout=60)
r.raise_for_status()
Path("output.mp4").write_bytes(r.content)
print("Saved output.mp4")
PYTHONOpen output.mp4 in a player. The file returned by the API is now the video your application can show or offer for download.
6. Review the result before shipping it
A completed job is ready for review. Watch the whole video for the intended action, recognizable character, hands and background movement. If your prompt requests speech, listen to the actual output as well.
Handle failures according to their cause. Authentication errors need a valid key; insufficient credits need a funded workspace; invalid inputs need a corrected request. For a rate limit, respect the server’s retry guidance. Do not treat all failures as reasons to start a new generation automatically.
Once the short example works, move key handling and submission into your server, keep job IDs in persistent storage and give users a clear progress state. Affogato’s API reference is the source for the complete contract; this tutorial’s job is to help you connect those steps into one usable video workflow.
A face-related restriction on another platform is different from an invalid file or missing API credit. Affogato’s support for real-person photos addresses the first obstacle; handle ordinary integration errors according to their actual cause.
API contract: Affogato OpenAPI and developer guide. Noor photographs created with NanoBanana Pro. Photo compositions by @kal_reston and @club_recess.


