Code examples

Midjourney API in PHP

Plain PHP with the curl extension, nothing to install via Composer. The same code can be dropped into Laravel or WordPress.

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.
  • PHP 7.4+ with the curl extension.

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:

statusWhat it means
sentwe passed the task on for processing
waitingMidjourney accepted it and put it in the queue
progressit is running, progress goes from 0 to 100
donefinished, the output is in result
errorfailed, see status_reason

Generate, upscale, save

Run from the console: USERAPI_KEY=your-key php client.php

<?php
// PHP 7.4+ with the curl extension. Run: USERAPI_KEY=... php client.php
const API_BASE = 'https://api.userapi.ai/midjourney/v2';
function userapi(string $method, string $path, ?array $payload = null): array
{
$ch = curl_init(API_BASE . $path);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => [
'api-key: ' . getenv('USERAPI_KEY'),
'Content-Type: application/json',
],
]);
if ($payload !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
}
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($body === false || $code >= 400) {
throw new RuntimeException("$path: HTTP $code $body");
}
return json_decode($body, true);
}
// Start a task and return its hash.
function start(string $method, array $payload): string
{
return userapi('POST', "/$method", $payload)['hash'];
}
// Poll the status endpoint until the task is done or failed.
function wait(string $hash, int $interval = 5, int $timeout = 600): array
{
$deadline = time() + $timeout;
while (time() < $deadline) {
$task = userapi('GET', '/status?hash=' . urlencode($hash));
if ($task['status'] === 'done') {
return $task;
}
if ($task['status'] === 'error') {
throw new RuntimeException('task failed: ' . $task['status_reason']);
}
echo $task['status'], ' ', $task['progress'] ?? 0, "%\n";
sleep($interval);
}
throw new RuntimeException("timeout waiting for $hash");
}
// 1. Generate a grid of four images
$grid = wait(start('imagine', ['prompt' => 'a red fox in a snowy forest, cinematic light --ar 3:2']));
echo 'grid: ', $grid['result']['url'], "\n";
// 2. Upscale the second image of the grid
$upscaled = wait(start('upscale', ['hash' => $grid['hash'], 'choice' => 2]));
// 3. Save it: Discord CDN links are not permanent storage
file_put_contents('fox.png', file_get_contents($upscaled['result']['url']));
echo "saved fox.png\n";

start() creates a task and returns the hash, wait() checks /status every 5 seconds. For upscale you need the grid's hash and the picture number (1-4).

Parameters like --ar, --v 7 or --sref are written into the prompt as in Discord. Prompts can be long (ours average about 570 characters), so build them with json_encode, as in the example, and don't glue the JSON together by hand.

Webhook

In a web app, pass webhook_url and we will call it when the task changes:

$hash = start('imagine', [
'prompt' => 'isometric cozy coffee shop, soft pastel colors',
'webhook_url' => 'https://your-server.com/webhook.php',
'webhook_type' => 'result', // or 'progress' for every progress update
]);

The body is the same JSON as from /status. The example below only writes to the log. In a real handler, download the file there and respond with 200:

<?php
// webhook.php: set https://your-server.com/webhook.php as webhook_url
$task = json_decode(file_get_contents('php://input'), true);
if (($task['status'] ?? '') === 'done') {
error_log("{$task['type']} {$task['hash']} {$task['result']['url']}");
} elseif (($task['status'] ?? '') === 'error') {
error_log("failed: {$task['hash']} {$task['status_reason']}");
}
http_response_code(200);

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_reasonWhat to do
not found an active Discord accountNo 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 start() with a different method name and payload. Parameters are in the documentation.

MethodEndpoint (POST /midjourney/v2…)Input
Imagine/imagineprompt
Upscale/upscalehash of imagine / blend / describe / animate + choice 1-4
Upsample (Subtle, Creative, 2x, 4x)/upsamplehash of upscale
Variation/variationhash of imagine / describe / blend + choice 1-4
Vary (Subtle) / Vary (Strong)/vary-subtle, /vary-stronghash of upscale
Reroll/rerollhash of a task
Animate (video)/animatehash of upscale + high / low
Inpaint (Vary Region)/inpainthash of upscale + mask + prompt
Zoom Out/zoomhash of upscale + 50 / 75 or prompt
Pan/panhash of upscale + direction
Blend/blend2-5 image URLs
Describe/describeimage URL
Seed/seedhash of a task
Upload/uploadPNG / JPG file or URL
Info, Speed, Prefer (Remix)/info, /speed, /preferaccount_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

Do I need Composer packages?

No, only the curl and json extensions, which are almost always installed. If your project already uses Guzzle, the same requests work through it.

WordPress, Laravel?

Yes. In WordPress use wp_remote_post / wp_remote_get, in Laravel the Http facade. Keep the key in the config, not in a theme or in JavaScript.

Polling or webhook?

In a web request, a webhook, so the page does not hang while Midjourney is working. Polling is fine for CLI scripts and cron.

See also

Use image generation Magic with UserAPI