Midjourney (MJ) API
MJ uses an asynchronous workflow: submit an Imagine request, save the task ID, poll its status, and read imageUrl when it succeeds.
Dev preview only. The production API is not available yet. Examples use https://teamorouter-dev.teamolab.com and require an API key and balance for that development environment. Production keys and balances are separate from dev.
Base URL and authentication
Use https://teamorouter-dev.teamolab.com without /v1. These endpoints use the generic MJ protocol and cannot be called with the OpenAI Images SDK's images.generate() method.
| Method | Path | Purpose |
|---|---|---|
| POST | /mj/submit/imagine |
Submit an Imagine task |
| GET | /mj/task/{task_id}/fetch |
Retrieve task status and results |
Both endpoints require a TeamoRouter API key:
Authorization: Bearer <TEAMO_API_KEY>
Content-Type: application/json
The examples read credentials from environment variables. Keep your key on your own server.
Submit Imagine
export TEAMO_API_BASE="https://teamorouter-dev.teamolab.com"
export TEAMO_API_KEY="<YOUR_DEV_API_KEY>"
curl --max-time 610 -D mj-submit-headers.txt \
"$TEAMO_API_BASE/mj/submit/imagine" \
-H "Authorization: Bearer $TEAMO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"botType": "MID_JOURNEY",
"prompt": "A tiny blue sailboat on a calm lake, watercolor illustration --ar 1:1",
"state": "my-mj-request-001"
}'
| Parameter | Required | Description |
|---|---|---|
prompt |
Yes | A nonempty image description. Ordinary MJ parameters such as --ar are allowed. |
botType |
No | Omit or set to MID_JOURNEY. NIJI_JOURNEY is not supported. |
model |
No | May be omitted. Only mj_imagine and mj_imagine-1k are accepted when provided. |
state |
No | Your correlation marker. It is not an idempotency key and does not prevent duplicate submissions. |
Example acceptance response:
{
"code": 1,
"description": "Submitted successfully",
"result": "1320098173412546"
}
Check all three conditions: HTTP success, numeric code equal to 1 (submitted) or 22 (queued), and a valid task ID in result. HTTP 200 alone does not mean the task was accepted; inspect description for other business codes. result may be a string or integer. Store it as a string for subsequent queries. Task IDs contain letters, digits, _, or -, with a maximum length of 128 characters.
Keep the x-request-id response header for billing and support. If X-Teamo-Task-Lookup: pending is present, keep the returned task ID and query later. A temporary 404 does not mean the submission failed and must not trigger another submission.
Retrieve the result
Replace TASK_ID with the result from the submission:
curl "$TEAMO_API_BASE/mj/task/TASK_ID/fetch" \
-H "Authorization: Bearer $TEAMO_API_KEY"
Poll about once every 10 seconds. The response is the MJ task object itself, without a data wrapper. Example successful result (excerpt):
{
"id": "1320098173412546",
"status": "SUCCESS",
"progress": "100%",
"imageUrl": "https://example.com/mj-result.png",
"failReason": ""
}
status |
Action |
|---|---|
Empty string, NOT_START, SUBMITTED, IN_PROGRESS, or another nonterminal state |
Keep the task ID and continue polling. 0% does not indicate failure. |
SUCCESS |
Stop polling, read imageUrl, and save the image promptly. |
FAILURE, CANCEL |
Stop polling and inspect failReason. |
The response may also contain upstream fields such as imageUrls and buttons. Returned buttons do not imply support for upscale or variation actions. Image URL domains and expiration depend on the upstream provider; permanent availability is not guaranteed.
Tasks are isolated by user. Another valid key belonging to the same user can retrieve the task. Lookup records are retained for 7 days from initial registration; polling does not extend retention. This is not a guarantee of 7-day image storage. Missing, expired, and other users' tasks all return 404.
Complete Python example
This uses the Python standard library, without the OpenAI SDK. It submits once and preserves the task ID if waiting times out. Resume with that ID instead of running the submission again.
import json
import os
import re
import time
import urllib.error
import urllib.request
base = os.environ.get("TEAMO_API_BASE", "https://teamorouter-dev.teamolab.com").rstrip("/")
key = os.environ["TEAMO_API_KEY"]
def request(path, body=None):
req = urllib.request.Request(
base + path,
data=json.dumps(body).encode() if body is not None else None,
headers={
"Authorization": "Bearer " + key,
"Content-Type": "application/json",
},
)
try:
with urllib.request.urlopen(req, timeout=610 if body is not None else 60) as response:
return json.load(response), response.headers
except urllib.error.HTTPError as error:
request_id = error.headers.get("x-request-id", "unknown")
detail = error.read().decode("utf-8", errors="replace")
raise RuntimeError(f"HTTP {error.code}; request_id={request_id}; {detail}") from error
# Submit once. Do not automatically retry after network errors or unknown outcomes.
submitted, headers = request("/mj/submit/imagine", {
"botType": "MID_JOURNEY",
"prompt": "A tiny blue sailboat on a calm lake --ar 1:1",
"state": "my-mj-request-001",
})
task_id = str(submitted.get("result") or "")
if submitted.get("code") not in (1, 22) or not re.fullmatch(r"[A-Za-z0-9_-]{1,128}", task_id):
raise RuntimeError(submitted)
print("Save task_id:", task_id, "request_id:", headers.get("x-request-id"), flush=True)
deadline = time.monotonic() + 900
while time.monotonic() < deadline:
time.sleep(10)
task, _ = request(f"/mj/task/{task_id}/fetch")
status = task.get("status", "")
print(status, task.get("progress"))
if status == "SUCCESS":
print(task["imageUrl"])
break
if status in ("FAILURE", "CANCEL"):
raise RuntimeError(task.get("failReason") or status)
else:
raise TimeoutError(f"Continue querying task {task_id}; do not submit again")
Pricing and supported features
The current 1K standard list price is $0.10 per accepted Imagine task. A response with code=1 or 22 and a valid task ID incurs one task charge, rather than a separate charge for each image in a four-image grid. Account multipliers, custom rates, or promotions may affect the actual amount.
Task queries do not incur another Imagine charge. Requests that were not accepted release the reservation. An accepted task that later becomes FAILURE or CANCEL is not automatically refunded in this version. Use the submission's x-request-id to locate its billing record.
Only 1K Imagine tasks are available. 2K / 4K, Niji, Action (upscale, variation, reroll), batch queries, and callbacks are not supported. Do not set notifyHook or select a provider or mode through accountFilter.channelId, instanceId, or modes.
These prompt options change cost or task quantity and currently return 400: --hd, --q / --quality, --repeat / --r, --fast, --turbo, --relax, and --niji, including --option=value syntax. Aspect ratio, version, and ordinary style parameters retain their MJ meaning and are validated upstream.
Errors and retries
| Condition | Action |
|---|---|
| 400 invalid parameters | Check prompt, model, botType, and unsupported cost options. |
| 401 | Use a valid TeamoRouter API key for the selected environment. |
| 402 insufficient balance or budget | Check the selected environment's balance and account budget. |
| 404 task not found | Check the ID, user, and 7-day retention period. Query later if the submission indicated pending. |
| 429 rate limit | Reduce request frequency. Submissions still follow the unknown-outcome rules below. |
| 5xx or connection timeout | Determine whether a task may already exist before sending another POST. |
If the error type is midjourney_submission_unknown, or the response includes X-Teamo-Submission-State: unknown or X-Should-Retry: false, a task may already have been created upstream. Disable automatic POST retries in your HTTP client, SDK, and proxy. Keep x-request-id and contact support to reconcile the outcome. If you have a task ID, keep querying that task.
There is no end-to-end Idempotency-Key deduplication guarantee. Repeating a POST with the same state or prompt may create and charge for another task. GET polling can continue; an unknown submission outcome or a wait timeout is not a reason to submit again.