Skip to main content
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_id immediately 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: photorealistic is clearer than realistic.
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

SourceRecommended approachNotes
User uploadStore it in your own OSS / CDN first, then pass that URL on submitDo not pass base64 directly (wastes bandwidth)
Public URLPass it directlyWatch out for SSRF (must be publicly reachable) and the 12 MiB limit
Third-party / other outputsRe-host to your own OSS firstThird-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

codeMeaningRetry strategy
1 / 200Success
4 VALIDATION_ERRORBad parameters❌ Do not retry; fix the parameters
3 NOT_FOUNDNo available instance / task_id does not existIf the instance is unavailable you can retry later; do not retry if the task_id does not exist
9 FAILUREService rejection / internal error⏳ Retryable, exponential backoff (1s, 4s, 16s)
21 MODALNon-terminal state✅ Keep calling /modal
24 BANNED_PROMPTSensitive word❌ Do not retry; change the prompt; already auto-refunded
429Rate limited⏳ Exponential backoff + jitter
5xx / network errorServer / 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 (white = 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 /modal within 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 SUBMITTED for a long time usually means it is queued.
  • Always include sleep when polling; do not spin in a tight loop without sleep.

Monitoring recommendations

MetricReference thresholdMeaning
Task SUCCESS rate (last 1h)> 95%Low values indicate service / network issues
Average completion time< 90sHigh values indicate queuing
Number of tasks stuck in MODALNear 0Many indicate the client did not call /modal
Proportion of code=24< 5%High values indicate prompts frequently trigger sensitive words

Troubleshooting checklist

SymptomWhere to look
Task stuck in SUBMITTED for a long timeQueued in the system; check again later
Task stuck in NOT_START for a long timeThe platform will auto-timeout and refund later; no manual action needed
Task in MODAL over 30 minutesThe client did not call /modal; it has been auto-cancelled (CANCEL) + refunded
prompt field is emptyThe text result of a describe task is in the description field
Missing one image in image_urlsContent moderation blocked part of the images; check fail_reason
Billing higher than expectedCheck the quota field; for video remember to multiply by batch_size