Get API Key Get API
Docs

Best Practices

Polling patterns, prompt design, image guidance, error-retry strategies, concurrency, and troubleshooting tips for Midjourney integration

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

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 /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

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.