Code examples
Midjourney API in Node.js
About 50 lines on plain fetch, no dependencies: generate an image, upscale it and save it to disk. Below that is an Express handler for webhooks.
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.
- Node.js 18+, since the code uses built-in
fetchand top-levelawait(hence.mjs).
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
Save as client.mjs, run with USERAPI_KEY=your-key node client.mjs:
// Node.js 18+ (built-in fetch). Run: USERAPI_KEY=... node client.mjsimport { writeFile } from 'node:fs/promises';const API_BASE = 'https://api.userapi.ai/midjourney/v2';const HEADERS = {'api-key': process.env.USERAPI_KEY,'Content-Type': 'application/json',};const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));// Start a task and return its hash.async function post(method, payload) {const res = await fetch(`${API_BASE}/${method}`, {method: 'POST',headers: HEADERS,body: JSON.stringify(payload),});if (!res.ok) throw new Error(`${method}: HTTP ${res.status} ${await res.text()}`);const { hash } = await res.json();return hash;}// Poll the status endpoint until the task is done or failed.async function wait(hash, { interval = 5000, timeout = 600000 } = {}) {const deadline = Date.now() + timeout;while (Date.now() < deadline) {const res = await fetch(`${API_BASE}/status?hash=${hash}`, { headers: HEADERS });if (!res.ok) throw new Error(`status: HTTP ${res.status}`);const task = await res.json();if (task.status === 'done') return task;if (task.status === 'error') {throw new Error(`task failed: ${task.status_reason} ${JSON.stringify(task.prefilter_result ?? [])}`);}console.log(task.status, `${task.progress ?? 0}%`);await sleep(interval);}throw new Error(`timeout waiting for ${hash}`);}// 1. Generate a grid of four imagesconst grid = await wait(await post('imagine', { prompt: 'a red fox in a snowy forest, cinematic light --ar 3:2' }));console.log('grid:', grid.result.url);// 2. Upscale the second image of the gridconst upscaled = await wait(await post('upscale', { hash: grid.hash, choice: 2 }));// 3. Save it: Discord CDN links are not permanent storageconst image = await fetch(upscaled.result.url);await writeFile('fox.png', Buffer.from(await image.arrayBuffer()));console.log('saved fox.png');
post() starts a task, wait() polls /status until done and throws on error. choice in upscale is the picture number in the grid, 1 to 4.
Midjourney parameters (--ar, --v, --sref, image URLs) are simply part of the prompt string. --ar is in 87% of the prompts people send us, and about a quarter use --sref. More numbers are on the Python page.
Webhook instead of polling (Express)
For a one-off script polling is fine. In a backend it is simpler to pass webhook_url:
const hash = await 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});
We send the same object that /status returns. With 'result' you get one call at the end, with 'progress' also the intermediate ones. Receiving it in Express:
// npm install expressimport express from 'express';const app = express();app.use(express.json({ limit: '1mb' }));app.post('/midjourney/webhook', (req, res) => {const task = req.body;if (task.status === 'done') {console.log(task.type, task.hash, task.result.url);} else if (task.status === 'error') {console.log('failed:', task.hash, task.status_reason);} else {console.log(task.hash, task.status, task.progress);}res.sendStatus(200);});app.listen(3000);
A batch of prompts
const prompts = ['a lighthouse at dawn', 'a lighthouse at noon', 'a lighthouse at night'];// Without account_hash, tasks are rotated across the Discord accounts linked to your API key.const results = await Promise.allSettled(prompts.map(async (prompt) => wait(await post('imagine', { prompt }))));results.forEach((r, i) => {console.log(prompts[i], '->', r.status === 'fulfilled' ? r.value.result.url : r.reason.message);});
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
Same post(), different method name and 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 npm package for Midjourney?
Midjourney itself has no public API, so there is no official package. For our API built-in fetch is enough. If you work in n8n, there is our community node n8n-nodes-userapi.
Can I call the API from the browser?
Better not: the api-key header would be visible to anyone who opens dev tools. Call it from your backend or a serverless function and give the browser only the finished image.
TypeScript, Next.js, serverless?
All fine, it is just HTTPS and JSON. In serverless functions use a webhook rather than waiting in a loop, because a generation can take longer than the function timeout.
How do I generate a batch of images?
Start the tasks and wait for them with Promise.allSettled, as in the example. With several Discord accounts on one key and no account_hash, the tasks are spread across the accounts.