Consolidated best practices for common questions, performance tuning, and error handling. Recommended reading before you integrate.
Task submission and polling
Submission endpoints are all asynchronous tasks: after submitting they return a task_id, then you periodically query GET /v1/midjourney/{task_id} for the status until SUCCESS / FAILURE.
import time, httpx
def wait_task(task_id, timeout=300):
deadline = time.time() + timeout
while time.time() < deadline:
resp = httpx.get(f"{HOST}/v1/midjourney/{task_id}",
headers={"Authorization": f"Bearer {API_KEY}"}).json()
if resp["status"] in ("SUCCESS", "FAILURE"):
return resp
if resp["status"] == "MODAL":
raise RuntimeError(f"task {task_id} needs a /modal call to supply params and complete")
time.sleep(3)
raise TimeoutError(task_id)- Polling cadence: 3–5s is recommended; higher frequency is pointless and wastes quota.
- Do not block synchronously in a web request waiting for the task to finish — return the
task_idimmediately after submitting and let the frontend poll asynchronously.
Prompt design
A good prompt:
a serene mountain lake at sunrise, photorealistic, soft golden light,
mist rising from water, snow-capped peaks in distance --ar 16:9 --v 8.1 --s 100- Subject first: lead with the subject, then describe the scene, and put modifiers last.
- Make structured params explicit: using
--ar/--v/--s(or the corresponding body fields) is more controllable than relying on defaults. - Avoid ambiguous words:
photorealisticis clearer thanrealistic.
Avoid: being overly abstract ("make it good"), scattered subjects (multiple parallel objects with no clear priority), and quoting words (they are treated as literal values).
Niji anime: pass niji: true + version: "7"; the platform normalizes it to --niji 7, and billing goes through midjourney@imagine-niji7.
Image-guidance best practices
| Source | Recommended approach | Notes |
|---|---|---|
| User upload | Store it in your own OSS / CDN first, then pass that URL on submit | Do not pass base64 directly (wastes bandwidth) |
| Public URL | Pass it directly | Watch out for SSRF (must be publicly reachable) and the 12 MiB limit |
| Third-party / other outputs | Re-host to your own OSS first | Third-party URLs may expire |
- Compress to \< 5 MiB: the platform limit is 12 MiB, but smaller images transfer / process faster.
- PNG / JPG / WebP are all fine; high-quality JPG is recommended.
- A resolution of 1024–2048 px is already enough; higher is wasteful.
- Image weight
iw(0–3, default 1): >1 stays closer to the source image, \<1 is more free.
Error handling and retry strategy
| code | Meaning | Retry strategy |
|---|---|---|
1 / 200 |
Success | ✅ |
4 VALIDATION_ERROR |
Bad parameters | ❌ Do not retry; fix the parameters |
3 NOT_FOUND |
No available instance / task_id does not exist | If the instance is unavailable you can retry later; do not retry if the task_id does not exist |
9 FAILURE |
Service rejection / internal error | ⏳ Retryable, exponential backoff (1s, 4s, 16s) |
21 MODAL |
Non-terminal state | ✅ Keep calling /modal |
24 BANNED_PROMPT |
Sensitive word | ❌ Do not retry; change the prompt; already auto-refunded |
429 |
Rate limited | ⏳ Exponential backoff + jitter |
5xx / network error |
Server / network | ⏳ Exponential backoff; for network errors you may retry once immediately |
import time, random, httpx
def submit_with_retry(payload, max_attempts=5):
for attempt in range(max_attempts):
try:
r = httpx.post(f"{HOST}/v1/midjourney/generations/imagine",
json=payload,
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=30)
data = r.json()
if r.status_code == 200 and data["code"] in (1, 200):
return data
if data["code"] in (4, 24):
raise ValueError(data["description"]) # not retryable
if data["code"] == 3 and "task" in data["description"]:
raise ValueError(data["description"]) # task_id does not exist
# the rest (9 / 429 / 5xx) are retryable
except httpx.RequestError:
pass
time.sleep((4 ** attempt) + random.uniform(0, 1)) # 1s / 4s / 16s ...
raise RuntimeError(f"reached max retry count {max_attempts}")Follow-up operation flow
# imagine → poll → upscale
imagine_id = submit({"prompt": "a cat"})["data"][0]["task_id"]
result = wait_task(imagine_id) # grid_image_url + 4 image_urls + buttons
upscale_id = submit_to("/upscale", {"task_id": imagine_id, "index": 2})["data"][0]["task_id"]
final = wait_task(upscale_id) # upscale is composed locally, 1–2s
single_image = final["image_urls"][0]Inpaint (two-step inpaint → modal):
imagine_id = submit({"prompt": "a portrait"})["data"][0]["task_id"]; wait_task(imagine_id)
upscale_id = submit_to("/upscale", {"task_id": imagine_id, "index": 1})["data"][0]["task_id"]; wait_task(upscale_id)
inpaint_id = submit_to("/inpaint", {"task_id": upscale_id})["data"][0]["task_id"] # status=modal
# Frontend draws the mask (transparent = repaint area), uploads it to your own OSS to get mask_url
final = submit_to("/modal", {
"task_id": inpaint_id,
"prompt": "replace the eyes with cybernetic blue eyes",
"mask_url": "https://your-oss.com/mask.png"
})
wait_task(final["data"][0]["task_id"])⚠️ After inpaint enters MODAL you must call
/modalwithin 30 minutes, otherwise the backend auto-cancels (CANCEL) and refunds.
Video billing control
- Single segment:
batch_size: 1→ charged 1 ×midjourney@video - Batch of 4 segments:
batch_size: 4→ charged 4 ×midjourney@video - HD single segment:
video_type: "vid_1.1_i2v_720"+batch_size: 1→ charged 1 ×midjourney@video-720p
Recommendation: if you only need 1 segment for delivery, use batch_size=1; only use 4 for batch comparison drafts. Do not default to 4 (it multiplies cost N times).
Concurrency and throughput
import asyncio
sem = asyncio.Semaphore(10) # client submits at most 10 concurrently
async def submit_one(prompt):
async with sem:
return await submit({"prompt": prompt})- The platform has a per-minute submission cap; exceeding it returns
429, which needs backoff retry. - Actual generation concurrency is determined by system capacity; exceeding it queues; a task staying in
SUBMITTEDfor a long time usually means it is queued. - Always include
sleepwhen polling; do not spin in a tight loop without sleep.
Monitoring recommendations
| Metric | Reference threshold | Meaning |
|---|---|---|
| Task SUCCESS rate (last 1h) | > 95% | Low values indicate service / network issues |
| Average completion time | \< 90s | High values indicate queuing |
| Number of tasks stuck in MODAL | Near 0 | Many indicate the client did not call /modal |
Proportion of code=24 |
\< 5% | High values indicate prompts frequently trigger sensitive words |
Troubleshooting checklist
| Symptom | Where to look |
|---|---|
Task stuck in SUBMITTED for a long time |
Queued in the system; check again later |
Task stuck in NOT_START for a long time |
The platform will auto-timeout and refund later; no manual action needed |
Task in MODAL over 30 minutes |
The client did not call /modal; it has been auto-cancelled (CANCEL) + refunded |
prompt field is empty |
The text result of a describe task is in the description field |
Missing one image in image_urls |
Content moderation blocked part of the images; check fail_reason |
| Billing higher than expected | Check the quota field; for video remember to multiply by batch_size |
This documentation is built and hosted on Mintlify, a developer documentation platform.