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:

http
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

bash
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:

json
{
  "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:

bash
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):

json
{
  "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.

python
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.

Готовы? Три шага, чтобы начатьВойдите в консоль · пополните баланс · создайте API Key
TelegramВопросы по подключению, оплате или маршрутизации? Мы в Telegram.