# Midjourney API на Python: генерация и сохранение

> Создайте задачу Midjourney на Python, опрашивайте статус и скачивайте сетку изображений. Какие операции доступны до начала интеграции.

- Canonical HTML: https://omniakey.com/ru/blog/midjourney-api-python
- Markdown: https://omniakey.com/ru/blog/midjourney-api-python.md
- Published: 2026-11-12 (updated 2026-11-12)
- Author: Kiki, COO at OmniaKey
- Language: ru
- Category: API setup
- Reading time: 8 min
- All posts: https://omniakey.com/blog/llms.txt

---

Пример вызывает из Python сторонний API OmniaKey для [Midjourney](https://omniakey.com/ru/midjourney) в три шага: создать задачу, опрашивать её статус до завершения и скачать PNG по подписанной ссылке на файл. Тестовый запуск интеграции 12 ноября 2026 года вернул изображение 2048x2048 за 197,1 секунды, и это изображение — сетка 2x2 с четырьмя вариантами. Скрипт ниже добавляет файл состояния, поэтому тайм-аут или неудачное скачивание не создают вторую платную задачу.

## Коротко

- Отправьте `POST https://api.omniakey.com/v1/media/tasks` с `Idempotency-Key`, затем опрашивайте `GET /v1/media/tasks/{id}` и скачайте `assets[0].url`.
- Одна задача возвращает одну PNG-сетку с четырьмя изображениями, а не четыре отдельных файла.
- Midjourney в этом API работает только с текстом в изображение: без редактирования, референсного изображения, blend, upscale, vary, сдвига и выбора версии.
- Ключ выдаёт OmniaKey, он отделён от подписки Midjourney. Сохраните тело и ключ идемпотентности до POST, добавьте id задачи, когда API примет запрос, и оставьте файл состояния, чтобы продолжить позже. Локальный тайм-аут не отменяет задачу на сервере.

## Какой API использует этот пример на Python?

Пример вызывает сторонний API OmniaKey для Midjourney. OmniaKey — не Midjourney, а ключ OmniaKey относится к отдельной учётной записи с собственным балансом. Подписка на сайте Midjourney — другой продукт, и ключ OmniaKey она не создаёт.

[Страница модели Midjourney](https://omniakey.com/ru/midjourney) показывает модель и текущую цену на стороне OmniaKey. Дальше разобраны эта интеграция и жизненный цикл её задач.

## Что понадобится

Нужны три вещи:

1. Ключ API OmniaKey, экспортированный как `OMNIAKEY_API_KEY`.
2. Python 3. Скрипт использует только стандартную библиотеку, поэтому SDK устанавливать не нужно.
3. `midjourney`, доступный для этого ключа/аккаунта. Если модели нет в списке ключа, проверьте доступ аккаунта до вызова создания.

```bash
export OMNIAKEY_API_KEY="your-key"
python3 midjourney_grid.py
```

Держите ключ вне скрипта и логов. Код читает его из окружения один раз.

## Тело запроса и параметры

Midjourney принимает промпт и три параметра. Тип бота предустановлен на стороне OmniaKey, поэтому тело может не содержать версию модели и `botType`.

|Поле|Значение|Примечание|
|-|-|-|
|`input.prompt`|Текст|Нужная сцена|
|`parameters.aspectRatio`|`1:1`|Также `16:9`, `9:16`, `3:2`, `2:3`|
|`parameters.quality`|`"1"`|В документации используется строка `"1"`|
|`parameters.n`|`1`|Должно быть 1|

Задавайте форму через `aspectRatio`, а не через размер в пикселях. Поле записывается в camelCase: `aspectRatio`, никогда `aspect_ratio`. Используйте одно из пяти значений выше.

Тело для примера промпта выглядит так:

```json
{
  "model": "midjourney",
  "type": "image",
  "operation": "generate",
  "input": {
    "prompt": "A small red paper boat floating on a quiet blue lake at sunrise, cinematic lighting, elegant composition, no text"
  },
  "parameters": {"aspectRatio": "1:1", "quality": "1", "n": 1}
}
```

## Вызов Midjourney API из Python

Сохраните его как `midjourney_grid.py` и запустите. Первый запуск записывает `midjourney-task.json` до POST, затем добавляет id задачи, когда OmniaKey принимает запрос.

```python
import hashlib
import json
import os
import sys
import time
import urllib.error
import urllib.request
import uuid
from pathlib import Path

API = "https://api.omniakey.com/v1"
KEY = os.environ["OMNIAKEY_API_KEY"]
STATE = Path("midjourney-task.json")
OUT = Path("midjourney-grid.png")
PROMPT = ("A small red paper boat floating on a quiet blue lake at sunrise, "
          "cinematic lighting, elegant composition, no text")


def save(state):
    tmp = STATE.with_suffix(".tmp")
    tmp.write_text(json.dumps(state, indent=2))
    tmp.replace(STATE)


def read_json(response):
    raw = response.read()
    try:
        return json.loads(raw)
    except ValueError:
        return {"raw": raw[:300].decode("utf-8", "replace")}


def call(method, path, body=None, headers=None):
    data = json.dumps(body).encode() if body is not None else None
    req = urllib.request.Request(
        API + path, data=data, method=method,
        headers={"Authorization": f"Bearer {KEY}", **(headers or {})})
    if body is not None:
        req.add_header("Content-Type", "application/json")
    try:
        with urllib.request.urlopen(req, timeout=60) as response:
            return response.status, read_json(response)
    except urllib.error.HTTPError as error:
        return error.code, read_json(error)
    except (urllib.error.URLError, TimeoutError) as error:
        return None, {"error": str(error)}


def main():
    state = json.loads(STATE.read_text()) if STATE.exists() else None
    if state is None:
        body = {
            "model": "midjourney", "type": "image", "operation": "generate",
            "input": {"prompt": PROMPT},
            "parameters": {"aspectRatio": "1:1", "quality": "1", "n": 1},
        }
        state = {"idempotency_key": str(uuid.uuid4()), "body": body,
                 "task_id": None, "status": "creating"}
        save(state)  # no API key here; write before POST
    elif state["body"]["input"]["prompt"] != PROMPT:
        sys.exit("The state file holds a different prompt. Move it aside to start a new job.")

    if OUT.exists() and state.get("status") == "downloaded":
        saved_sha = state.get("download_sha")
        if not saved_sha or hashlib.sha256(OUT.read_bytes()).hexdigest() == saved_sha:
            print("already saved", OUT)
            return

    if not state["task_id"]:
        code, task = call("POST", "/media/tasks", state["body"],
                          {"Idempotency-Key": state["idempotency_key"]})
        if code is None:
            sys.exit(f"Create not confirmed: {task['error']}. Rerun to retry the same key and body.")
        if code == 409:
            sys.exit("409 idempotency_key_reused: same key, different body. Restore the saved body.")
        if code == 503:
            sys.exit("503 no_available_channel: check model access for this key/account; "
                     "contact support with the request id if it persists.")
        if code not in (200, 202) or "id" not in task:
            sys.exit(f"Create failed: HTTP {code} {task}")
        state.update(task_id=task["id"], status=task["status"])
        save(state)

    deadline = time.monotonic() + 900
    while True:
        code, task = call("GET", f"/media/tasks/{state['task_id']}")
        if code is None:
            sys.exit(f"Polling stopped: {task['error']}. The job is still saved; rerun this script.")
        if code != 200 or "status" not in task:
            sys.exit(f"Poll failed: HTTP {code} {task}")
        state["status"] = task["status"]
        save(state)
        if task["status"] in ("failed", "canceled"):
            sys.exit(f"Job {task['status']}: {task.get('error')}")
        if task["status"] == "succeeded":
            break
        if time.monotonic() > deadline:
            sys.exit(f"Still {task['status']} after 900 s. Rerun with the same state file.")
        time.sleep(min(8, 2 + task.get("progress", {}).get("percent", 0) // 20))

    asset = task["assets"][0]
    if asset.get("mime_type") != "image/png":
        sys.exit(f"Expected a PNG grid, got {asset.get('mime_type')}")
    try:
        with urllib.request.urlopen(asset["url"], timeout=120) as response:
            data = response.read()  # signed URL: no Authorization header
    except (urllib.error.HTTPError, urllib.error.URLError, TimeoutError) as error:
        sys.exit(f"Download failed: {error}. Rerun to get a fresh signed URL and retry.")
    digest = hashlib.sha256(data).hexdigest()
    if asset.get("sha256") and digest != asset["sha256"]:
        sys.exit("SHA-256 mismatch. Rerun to download the asset again.")
    part = OUT.with_suffix(".part")
    part.write_bytes(data)
    part.replace(OUT)
    state.update(status="downloaded", download_sha=digest)
    save(state)
    print("saved", OUT, len(data), "bytes")


if __name__ == "__main__":
    main()
```

Формы запроса и ответа взяты из тестового запуска 12 ноября; состояние для перезапуска — добавление здесь, а его ветки жизненного цикла проверены офлайн на имитированных ответах.

Файл состояния хранит то, что нужно для продолжения: JSON-тело и UUID, используемый как ключ идемпотентности. Ключа API там нет. Файл записывается до POST, поэтому оборванное соединение оставляет оба значения на диске для восстановления.

Вызов создания может вернуть два кода успеха. `202` принимает новую задачу. `200` означает, что OmniaKey уже видел тот же ключ и то же тело и возвращает исходную задачу; продолжайте опрашивать эту задачу. Повторно отправляйте то же тело и ключ, только если результат создания неизвестен после сбоя соединения. Для изменённого тела документация указывает `409 idempotency_key_reused`.

Управляйте циклом опроса по `status`. Процент приблизительный и может оставаться на 0, пока задача выполняется. Останавливайтесь на `succeeded`, `failed` или `canceled`. [Документация OmniaKey о жизненном цикле медиазадач](https://docs.omniakey.com/ru/media/overview) (проверена 12 ноября 2026 года) описывает эти состояния.

Лимит опроса локальный. Когда он истекает, задача всё ещё выполняется или стоит в очереди; повторный запуск того же скрипта читает сохранённый id и продолжает опрос. Локальный тайм-аут не отменяет задачу. Для отмены есть отдельный endpoint.

## Возвращаемая сетка изображений Midjourney

У успешной задачи один файл: PNG-сетка. Вот образец из тестового запуска 12 ноября 2026 года:

![Четыре красных бумажных кораблика на синей воде на рассвете, расположены сеткой 2x2 в тёплом отражённом свете](https://omniakey.com/_img/blog/midjourney-api-python/paper-boat-grid.03735622.1600.webp)

_Образец тестового запуска 12 ноября 2026 года: одна задача midjourney, один PNG 2048x2048, четыре варианта, 197,1 секунды от создания до скачивания. Скачанные байты совпали с SHA-256 задачи._

Ответ ниже сокращён: ссылка скрыта, а `expires_at` — историческое значение. В нём добавлены нужные поля файла:

```json
{
  "id": "task_DbCWJPfPL1jyhSD38IihNHiiFQao8hjV",
  "status": "succeeded",
  "progress": {"percent": 100, "stage": "completed"},
  "assets": [
    {
      "kind": "image",
      "mime_type": "image/png",
      "width": 2048,
      "height": 2048,
      "byte_size": 5834301,
      "sha256": "f503cef038d15fa150b36b5fe7c41874febad3aa1167698d790781e47a4a10b9",
      "expires_at": "2026-11-12T04:16:41Z",
      "url": "https://…redacted-signed-url…"
    }
  ]
}
```

Читайте `width` и `height` из файла, а не задавайте 2048 жёстко. Квадрат 2048x2048 и 197,1 секунды — один образец; относитесь к ним именно так. Все четыре варианта находятся в одном PNG, поэтому разделите сетку после скачивания, если приложению нужны отдельные изображения.

`url` подписана и живёт недолго, около часа. Запись задачи и изображение хранятся 24 часа после создания, согласно [документации API изображений](https://docs.omniakey.com/ru/media/image) OmniaKey (проверена 12 ноября 2026 года). Скачивайте до истечения ссылки. Если она истекла, опросите id задачи ещё раз, чтобы получить новую подписанную ссылку.

Скачивайте файл по ссылке обычным GET без заголовка `Authorization`. Подпись находится в самой ссылке. Скрипт также сравнивает скачанные байты с `sha256` задачи, поэтому обрезанный файл обнаруживается до перезаписи хорошей копии.

## Повторный запуск после тайм-аута или неудачного скачивания

Запустите ту же команду и сохраните задачу. Скрипт читает `midjourney-task.json` и выбирает самый короткий безопасный путь:

- Создание не подтверждено: он снова отправляет POST с тем же телом и тем же ключом идемпотентности.
- Id задачи есть, задача ещё выполняется: он опрашивает существующий id.
- Задача завершилась, но PNG отсутствует: он один раз опрашивает задачу ради новой подписанной ссылки и скачивает снова.
- PNG уже сохранён и совпадает с записанным SHA-256: он завершается без нового запроса. Повреждённый файл опрашивается и скачивается заново.

Это работает в пределах 24-часового хранения. Тот же ключ с другим промптом или параметрами даёт `409`. Скрипт отказывается от файла состояния, если промпт больше не совпадает; уберите файл, если меняете параметры.

## Ошибки и решения

Сбои ниже следуют из правил задач и идемпотентности API. Исправьте причину и, где применимо, запустите снова с тем же файлом состояния.

|Ответ|Код|Что делать|
|-|-|-|
|400|`missing_idempotency_key`|Добавьте заголовок; держите его короче 255 символов|
|409|`idempotency_key_reused`|Отправьте исходное тело или начните новую задачу с новым ключом|
|503|`no_available_channel`|Проверьте доступ для этого ключа или аккаунта; если не проходит, обратитесь в поддержку|
|`failed`|`error.code`, `error.message`|Прочитайте ошибку задачи; деньги за неудачные задачи возвращаются|
|`canceled`|—|Задача остановлена; создайте новую, если она ещё нужна|

`503 no_available_channel` не означает, что у ключа нет разрешения на модель. Две отправки с одним ключом из этого пакета вернули `503` и не создали задачу; другой ключ успешно сработал в более раннем тестовом запуске того же дня. Причина неизвестна: это может быть доступность ключа/аккаунта или доступность вышестоящего канала. Проверьте, доступен ли `midjourney` вашему текущему ключу/аккаунту. Если `503` повторяется, отправьте поддержке тело ответа и id запроса.

`200` при создании — это повтор исходной задачи. Терминальными сбоями считаются только `failed` и `canceled`; если остановить скрипт, задача продолжит выполняться.

## Стоимость и когда выбрать другой API изображений

OmniaKey берёт плату один раз за успешную задачу, а деньги за неудачные и отменённые задачи возвращаются автоматически. Текущая цена указана на [странице модели Midjourney](https://omniakey.com/ru/midjourney); проверьте её перед оценкой пакета.

Midjourney подходит, когда нужно выбрать один из четырёх вариантов в одной сетке. Эта интеграция возвращает такую сетку из текста в изображение; если нужна одна картинка за вызов или редактирование с сохранением исходной сцены, посмотрите [GPT Image 2.5 API](https://omniakey.com/ru/blog/gpt-image-2-5-api) или [Nano Banana Pro API](https://omniakey.com/ru/blog/nano-banana-pro-api).

Если проще ввести промпт, чем писать код, [AI Image Generator](https://omniakey.com/ru/ai-image-generator) запускает модели изображений в браузере.

## Частые вопросы

### Нужна ли подписка Midjourney для этого API?

Нет. Ключ выдаёт OmniaKey, а вызовы идут в сторонний API OmniaKey. OmniaKey — не Midjourney; подписка на сайте Midjourney — другой продукт, и ключ OmniaKey она не создаёт.

### Почему одна задача Midjourney возвращает четыре изображения в одном файле?

Midjourney возвращает сетку 2x2 с четырьмя вариантами одним PNG. Все четыре используют этот файл, а upscale и vary в этой интеграции недоступны, поэтому разделите сетку сами после скачивания, если нужны отдельные файлы.

### Что означает 409 idempotency\_key\_reused?

Вы отправили ключ идемпотентности, который у OmniaKey уже есть, но тело запроса изменилось. Отправьте исходное тело или начните новую задачу с новым ключом. Обычная причина — повторное использование ключа с другим промптом.

### Что будет, если скрипт не дождётся ответа при опросе?

Задача продолжит выполняться. Id задачи лежит в `midjourney-task.json`, поэтому повторный запуск того же скрипта опросит этот id или скачает готовый файл. Локальный тайм-аут оставляет задачу живой, а оплату — в силе.
