Generations
Follow a Direct API generation to its final status by long-polling, read several at once, and know when to poll.
A generation is one unit of asynchronous work: one image, one video, one audio track, one render. Tools with returns_generation: true start one or more and answer at once with their ids. Follow them by polling, by webhook, or both: use a webhook as the main signal and an occasional poll as the safety net.
Statuses
queued ──► in_progress ──► succeeded
│ │
│ ├──────► failed
└─────────────┴──────► cancelled| Status | Final | Meaning |
|---|---|---|
queued | no | accepted, waiting for model capacity |
in_progress | no | the model is working |
succeeded | yes | assets holds the output |
failed | yes | error says why; the charge is refunded and billing.credits_refunded says how much |
cancelled | yes | stopped before it produced output; the charge is refunded |
Treat any status you do not recognise as not final and keep following the generation; new intermediate statuses may be added.
The generation object
{
"object": "generation",
"id": "0192a7b3-a1c4-7e02-8f9d-1a2b3c4d5e6f",
"tool": "generate_video",
"status": "succeeded",
"project_id": "01928f4e-5b7c-7d2a-9e11-3c4b5a6d7e8f",
"model_id": "mdl_7rk2pq9vxa4m",
"created_at": "2026-10-25T09:14:03.120Z",
"completed_at": "2026-10-25T09:15:41.902Z",
"error": null,
"billing": {
"estimated_credits": 120,
"credits_charged": 96,
"credits_refunded": 0,
"credits_are_final": true,
"credits_settle_on_output_duration": true
},
"assets": [
{
"id": "0192a7b4-0f1e-7d3c-9b8a-7c6d5e4f3a2b",
"type": "video",
"name": "Bicycle hero shot",
"mime_type": "video/mp4",
"width": 1920,
"height": 1080,
"duration_seconds": 8,
"url": "https://mcp.morphic.com/m/Xk29dPq",
"url_expires_at": null,
"studio_url": "https://studio.morphic.com/…",
"final_generation_expires_at": null
}
],
"content_left_out": [],
"render_warnings": [],
"metadata": { "order_id": "4411" },
"poll_after_seconds": null
}| Field | Meaning |
|---|---|
error | null, or { "error_code": "…", "message": "…" } on failed. The message is safe to show your users. |
billing | what this generation cost; see Credits and usage |
assets[].id | the asset id other tools take as input (image_asset_id, video_asset_id, …) |
assets[].url | a short link to download the media; it does not expire, and url_expires_at is null |
assets[].studio_url | opens the asset in Morphic Studio for your team |
assets[].final_generation_expires_at | draft videos only: until when finalize_video_draft can render it at full resolution |
content_left_out, render_warnings | composes and timeline exports only: what the output could not include, and why |
poll_after_seconds | while not final, how long to wait before the next read; null once final |
Read one generation
GET /v1/direct/generations/{id} answers 200 with { "generation": { … } }.
curl -s "https://mcp.morphic.com/v1/direct/generations/$ID?wait=45" \
-H "Authorization: Bearer $MORPHIC_API_KEY"waitis optional,0to45seconds. The request is held open until the generation's status changes or it finishes, then answers at once.- When the wait runs out, it answers with the current state. A value above 45 is clamped to 45.
- A generation of another organization, or one this key may not read, answers
404, never403, so ids cannot be probed.
Read several generations
POST /v1/direct/generations/batch-get reads up to 10 generations in one request.
curl -s https://mcp.morphic.com/v1/direct/generations/batch-get \
-H "Authorization: Bearer $MORPHIC_API_KEY" \
-H "Content-Type: application/json" \
-d '{"ids": ["0192a7b3-…", "0192a7b3-…"], "wait": 30}'- With
wait, the request answers as soon as any of them changes status or all of them are final. - The response is
{ "generations": [ … ], "not_found": ["…"] }, in the order asked.
How often to poll
Each start and each non-final read carries poll_after_seconds. Wait at least that long before the next read, then back off.
| Output | First poll after | Typical time to final |
|---|---|---|
| image, image edit, upscale, image operations | 5 s | 5 to 60 s |
| speech, sound effect, transcription | 10 s | 10 to 90 s |
| music, dialogue | 20 s | 30 s to 3 min |
| video, video edit, lip sync, renders | 30 s | 1 to 10 min, longer under load |
A long-poll (wait=45) costs one read however long it is held, so it is both the fastest and the cheapest way to poll. Set your HTTP client's timeout above the wait: 60 s for wait=45.
import time, requests
def wait_for(generation_id, key):
headers = {"Authorization": f"Bearer {key}"}
url = f"https://mcp.morphic.com/v1/direct/generations/{generation_id}"
while True:
r = requests.get(url, params={"wait": 45}, headers=headers, timeout=60)
if r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", "5")))
continue
r.raise_for_status()
gen = r.json()["generation"]
if gen["status"] in ("succeeded", "failed", "cancelled"):
return gen
# A long-poll already waited; only sleep if the server asks for more.
time.sleep(max(0, (gen.get("poll_after_seconds") or 0) - 45))Polling limits
| Limit | Value | On breach |
|---|---|---|
| Reads of one generation by one key | 30 per minute | 429, Retry-After |
| Reads of all generations by one key | 1,200 per minute | 429, Retry-After |
wait | 45 seconds | clamped |
| Ids per batch read | 10 | 400 |
A batch read counts once per id against both limits. At the recommended cadence one key can follow several hundred generations in flight at once; if you follow more, use webhooks.
If you have any trouble or have any questions, feel free to reach out. We're happy to help. Write to us at support@morphic.com.