Code examples
Midjourney API in Python
There is no SDK here and you don't need one. Below is a small script on top of requests that generates an image, upscales it and saves the file. After it come a webhook handler and a parallel version.
Updated
Before you start
- An API key from UserAPI.ai (how to get it). The first 50 requests are free.
- A Discord account with a paid Midjourney plan, added in the dashboard. If you haven't done that yet, follow the first steps.
- Python 3.8 or newer,
pip install requests(andhttpxfor the async example).
The examples read the key from the environment: export USERAPI_KEY=your-key.
How it works
Generation is asynchronous. A POST to a method like /midjourney/v2/imagine doesn't wait for the image. It returns a task hash straight away, and Midjourney does the work in the background.
There are two ways to get the result. You can ask GET /midjourney/v2/status?hash=… every few seconds, or you can pass webhook_url and we will POST the task to your server when it changes. Either way you get the same JSON. Most of our users poll: webhooks are set on about one task in five. Once status is done, result has the image URL and its size. That task's hash is what you pass to the next step (upscale, variation, zoom and so on).
Possible statuses:
| status | What it means |
|---|---|
sent | we passed the task on for processing |
waiting | Midjourney accepted it and put it in the queue |
progress | it is running, progress goes from 0 to 100 |
done | finished, the output is in result |
error | failed, see status_reason |
Generate, upscale, save
imagine returns a grid of four pictures. The script waits for it, upscales picture number 2 and writes it to fox.png. Midjourney flags like --ar or --v are just part of the prompt.
import osimport timeimport requestsAPI_BASE = "https://api.userapi.ai"HEADERS = {"api-key": os.environ["USERAPI_KEY"],"Content-Type": "application/json",}def post(method: str, payload: dict) -> str:"""Start a task and return its hash."""r = requests.post(f"{API_BASE}/midjourney/v2/{method}", json=payload, headers=HEADERS, timeout=30)r.raise_for_status()return r.json()["hash"]def wait(task_hash: str, interval: float = 5, timeout: float = 600) -> dict:"""Poll the status endpoint until the task is done or failed."""deadline = time.time() + timeoutwhile time.time() < deadline:r = requests.get(f"{API_BASE}/midjourney/v2/status", params={"hash": task_hash}, headers=HEADERS, timeout=30)r.raise_for_status()task = r.json()if task["status"] == "done":return taskif task["status"] == "error":raise RuntimeError(f"task failed: {task.get('status_reason')} {task.get('prefilter_result')}")print(f"{task['status']} {task.get('progress') or 0}%")time.sleep(interval)raise TimeoutError(task_hash)def download(url: str, path: str) -> None:with requests.get(url, stream=True, timeout=60) as r:r.raise_for_status()with open(path, "wb") as f:for chunk in r.iter_content(65536):f.write(chunk)if __name__ == "__main__":# 1. Generate a grid of four imagesgrid = wait(post("imagine", {"prompt": "a red fox in a snowy forest, cinematic light --ar 3:2"}))print("grid:", grid["result"]["url"])# 2. Upscale the second image of the gridupscaled = wait(post("upscale", {"hash": grid["hash"], "choice": 2}))# 3. Save it: Discord CDN links are not permanent storagedownload(upscaled["result"]["url"], "fox.png")print("saved fox.png")
post() starts a task and gives back its hash. wait() asks for the status until the task is done (or raises on error). For upscale you pass the grid's hash and the picture number, 1 to 4. Status calls are not billed.
Midjourney parameters in the prompt
The prompt goes to Midjourney exactly as you send it, so all parameters work the same way as in Discord: aspect ratio, version, style references, image prompts. For a sense of what people actually send, here is how often parameters appear in successful prompts through our API:
| In the prompt | Share of prompts |
|---|---|
--ar | 87% |
--v 7 | 34% |
--style raw | 33% |
--sref | 24% |
image URL at the start | 15% |
--cref | 7% |
--oref | 5% |
Prompts are long, too: the average is about 570 characters. Midjourney checks parameter combinations itself, so a conflict like --oref with --version 8.1 comes back as an error with its message in status_reason.
Webhook instead of polling (Flask)
If the code runs on a server, it is easier to let us call you. Add webhook_url when starting the task:
task_hash = post("imagine", {"prompt": "isometric cozy coffee shop, soft pastel colors","webhook_url": "https://your-server.com/midjourney/webhook","webhook_type": "result", # or "progress" for every progress update})
The body we POST is the same JSON as the status response. "result" means one call when the task is finished or failed, "progress" adds the intermediate updates. A minimal receiver:
from flask import Flask, requestapp = Flask(__name__)@app.post("/midjourney/webhook")def midjourney_webhook():task = request.get_json()if task["status"] == "done":print(task["type"], task["hash"], task["result"]["url"])elif task["status"] == "error":print("failed:", task["hash"], task.get("status_reason"))else:print(task["hash"], task["status"], task.get("progress"))return "", 200
Several prompts in parallel
The work happens on Midjourney's side, so nothing stops you from starting a few tasks and waiting for all of them. With httpx and asyncio:
import asyncioimport osimport httpxAPI_BASE = "https://api.userapi.ai/midjourney/v2"HEADERS = {"api-key": os.environ["USERAPI_KEY"]}async def generate(client: httpx.AsyncClient, prompt: str) -> str:r = await client.post(f"{API_BASE}/imagine", json={"prompt": prompt})r.raise_for_status()task_hash = r.json()["hash"]while True:await asyncio.sleep(5)task = (await client.get(f"{API_BASE}/status", params={"hash": task_hash})).json()if task["status"] == "done":return task["result"]["url"]if task["status"] == "error":raise RuntimeError(task.get("status_reason"))async def main():prompts = ["a lighthouse at dawn", "a lighthouse at noon", "a lighthouse at night"]async with httpx.AsyncClient(headers=HEADERS, timeout=30) as client:urls = await asyncio.gather(*(generate(client, p) for p in prompts))for prompt, url in zip(prompts, urls):print(prompt, "->", url)asyncio.run(main())
Errors
When a task ends with status: "error", the reason is in status_reason. Log it. Sending the same prompt again and again usually doesn't help. Midjourney's own messages are passed through as they are. These are the ones we see most often:
| status_reason | What to do |
|---|---|
not found an active Discord account | No working Discord account on this key. Open Discord accounts in the dashboard and check the status. |
You've run out of hours on your plan for this month… | Fast hours are used up. Switch the account to relax with the speed method or buy more hours in Midjourney. |
You've reached the maximum number of concurrent jobs… | Midjourney is already running as many jobs as your plan allows. Wait, or add more accounts to the key. |
Your job queue is full… | Same story: too many jobs at once on one account. |
Your Midjourney subscription is paused because of a payment issue… | Payment problem on the Midjourney side. Fix it in your Midjourney account. |
You have a pending moderation message… | Open Discord and acknowledge the message from Midjourney, then the account works again. |
`--oref` is not compatible with `--version 8.1` | A parameter conflict in the prompt. Remove the parameter or change the version. |
Before a prompt goes to Midjourney we run an eligibility check on it. If the prompt doesn't pass, the task is not sent and prefilter_result lists why: TOXICITY, SEVERE_TOXICITY, IDENTITY_ATTACK, INSULT, PROFANITY, THREAT, SEXUALLY_EXPLICIT, FLIRTATION. An empty array means the check passed. The check is there to help your Discord account last longer. If you really need to, it can be turned off for imagine with "is_disable_prefilter": true.
Result URLs point to Discord's CDN. Download the files you want to keep instead of storing the links.
If tasks stop going through, check the account itself: GET /discord/info?account_hash=… returns its status. tos means you need to open Discord and accept the Midjourney terms, and banned means the account was banned.
Other methods
They all work like imagine and upscale above: change the method name in post() and the payload. Parameters are in the documentation.
| Method | Endpoint (POST /midjourney/v2…) | Input |
|---|---|---|
| Imagine | /imagine | prompt |
| Upscale | /upscale | hash of imagine / blend / describe / animate + choice 1-4 |
| Upsample (Subtle, Creative, 2x, 4x) | /upsample | hash of upscale |
| Variation | /variation | hash of imagine / describe / blend + choice 1-4 |
| Vary (Subtle) / Vary (Strong) | /vary-subtle, /vary-strong | hash of upscale |
| Reroll | /reroll | hash of a task |
| Animate (video) | /animate | hash of upscale + high / low |
| Inpaint (Vary Region) | /inpaint | hash of upscale + mask + prompt |
| Zoom Out | /zoom | hash of upscale + 50 / 75 or prompt |
| Pan | /pan | hash of upscale + direction |
| Blend | /blend | 2-5 image URLs |
| Describe | /describe | image URL |
| Seed | /seed | hash of a task |
| Upload | /upload | PNG / JPG file or URL |
| Info, Speed, Prefer (Remix) | /info, /speed, /prefer | account_hash |
Try it on 50 free requests
Register, add a Discord account that has Midjourney, and you get an API key. The first 50 requests are free and you don't need to enter a card. Since 2023 more than 10,000 people have signed up and sent over 8 million requests through the API.
Questions
Is there an official Midjourney library for Python?
No, Midjourney has no public API and no SDK. Our API is plain REST, so requests or httpx is enough.
How long does one image take?
Depends on how busy Midjourney is and on the account speed mode (relax, fast or turbo, switched with the speed method). Polling every 5 seconds is a sensible default.
Can I run several prompts at once?
Yes, see the asyncio example above. If several Discord accounts are linked to the key and you don't pass account_hash, the tasks are spread between them.
Does Discord need to be open on my machine?
No. The Discord account is added once in the dashboard, after that your script only talks to api.userapi.ai.