# Midjourney API mit Python: Bilder erzeugen und speichern

> Midjourney-Aufgabe in Python anlegen, Status abfragen und das Bildraster herunterladen. Welche Operationen die API bietet, bevor du sie einbaust.

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

---

Ein [Midjourney](https://omniakey.com/de/midjourney)-Job auf OmniaKey braucht in Python drei Schritte: Aufgabe anlegen, Status bis zum Abschluss abfragen und das PNG von der signierten Asset-URL herunterladen. Ein Integrationstest am 12. November 2026 lieferte ein Bild mit 2048x2048 Pixeln in 197,1 Sekunden, und dieses Bild ist ein 2x2-Raster mit vier Varianten. Das Skript unten ergänzt eine Statusdatei, damit ein Timeout oder ein fehlgeschlagener Download keinen zweiten bezahlten Job erzeugt.

## Kurz gesagt

- Sende `POST https://api.omniakey.com/v1/media/tasks` mit einem `Idempotency-Key`, frage dann `GET /v1/media/tasks/{id}` ab und lade `assets[0].url` herunter.
- Eine Aufgabe liefert ein PNG-Raster mit vier Bildern, keine vier getrennten Dateien.
- Midjourney macht in dieser API nur Text zu Bild: kein Editieren, Referenzbild, Blend, Upscale, Vary, Verschieben und keine Versionsauswahl.
- Der Schlüssel kommt von OmniaKey und ist von einem Midjourney-Abo getrennt. Speichere Body und Idempotenzschlüssel vor dem POST, ergänze die Task-ID, sobald die API die Anfrage annimmt, und behalte die Statusdatei, um später fortzufahren. Ein lokaler Timeout bricht die Aufgabe auf dem Server nicht ab.

## Welche API nutzt dieses Python-Beispiel?

Das Beispiel ruft OmniaKeys Drittanbieter-API für Midjourney auf. OmniaKey ist nicht Midjourney, und ein OmniaKey-Schlüssel gehört zu einem eigenen Konto mit eigenem Guthaben. Ein Abo auf der Midjourney-Website ist ein separates Produkt und erzeugt keinen OmniaKey-Schlüssel.

Die [Midjourney-Modellseite](https://omniakey.com/de/midjourney) zeigt das Modell und den aktuellen Preis auf OmniaKeys Seite. Der Rest der Anleitung behandelt diese Integration und den Ablauf ihrer Aufgaben.

## Was du brauchst

Drei Dinge:

1. Einen OmniaKey-API-Schlüssel, exportiert als `OMNIAKEY_API_KEY`.
2. Python 3. Das Skript nutzt nur die Standardbibliothek, du musst also kein SDK installieren.
3. `midjourney` für diesen Schlüssel/dieses Konto. Fehlt das Modell in der Modellliste des Schlüssels, prüfe den Kontozugriff, bevor du den Create-Aufruf startest.

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

Halte den Schlüssel aus dem Skript und aus Logs heraus. Der Code liest ihn einmal aus der Umgebung.

## Request-Body und Parameter

Midjourney bekommt einen Prompt und drei Parameter. Der Bot-Typ ist auf OmniaKeys Seite voreingestellt, deshalb kann der Body Modellversion und `botType` weglassen.

|Feld|Wert|Hinweis|
|-|-|-|
|`input.prompt`|Text|Die gewünschte Szene|
|`parameters.aspectRatio`|`1:1`|Auch `16:9`, `9:16`, `3:2`, `2:3`|
|`parameters.quality`|`"1"`|Die Docs nutzen die Zeichenkette `"1"`|
|`parameters.n`|`1`|Muss 1 sein|

Lege die Form mit `aspectRatio` fest, nicht mit einer Pixelgröße. Das Feld ist camelCase: `aspectRatio`, nie `aspect_ratio`. Nutze einen der fünf genannten Werte.

Der Body für den Beispiel-Prompt sieht so aus:

```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}
}
```

## Das Python-Skript

Speichere es als `midjourney_grid.py` und führe es aus. Der erste Lauf schreibt `midjourney-task.json` vor dem POST und ergänzt die Task-ID, sobald OmniaKey die Anfrage annimmt.

```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()
```

Die Formen von Request und Response stammen aus dem Integrationstest vom 12. November; der Neustart-Status ist hier die Ergänzung, und seine Ablaufpfade wurden offline mit simulierten Antworten geprüft.

Die Statusdatei bewahrt, was du zum Fortsetzen brauchst: den JSON-Body und die UUID als Idempotenzschlüssel. Einen API-Schlüssel speichert sie nicht. Sie wird vor dem POST geschrieben, sodass eine abgebrochene Verbindung beide Werte zur Wiederherstellung auf der Platte lässt.

Der Create-Aufruf kann zwei Erfolgscodes liefern. `202` nimmt einen neuen Job an. `200` bedeutet, dass OmniaKey denselben Schlüssel und denselben Body schon gesehen hat und die ursprüngliche Aufgabe zurückgibt; frage diese Aufgabe weiter ab. Sende denselben Body und Schlüssel nur erneut, wenn das Ergebnis der Erstellung nach einem Verbindungsfehler unklar ist. Die Docs nennen `409 idempotency_key_reused` für einen geänderten Body.

Steuere die Abfrageschleife über `status`. Die Prozentangabe ist ein Richtwert und kann bei 0 bleiben, während der Job läuft. Stoppe bei `succeeded`, `failed` oder `canceled`. OmniaKeys [Dokumentation zum Ablauf von Media-Aufgaben](https://docs.omniakey.com/en/media/overview) (geprüft am 12. November 2026) beschreibt diese Zustände.

Das Abfragelimit ist lokal. Läuft es ab, ist der Job weiter aktiv oder in der Warteschlange; ein erneuter Lauf desselben Skripts liest die gespeicherte Task-ID und fragt weiter ab. Ein lokaler Timeout ist kein Abbruch. Für den Abbruch gibt es einen eigenen Endpoint.

## Was zurückkommt

Eine erfolgreiche Aufgabe hat ein Asset: ein PNG-Raster. Das ist die Probe aus dem Integrationstest vom 12. November 2026:

![Vier gefaltete rote Papierboote auf blauem Wasser bei Sonnenaufgang, in einem 2x2-Raster mit warmem reflektiertem Licht](https://omniakey.com/_img/blog/midjourney-api-python/paper-boat-grid.03735622.1600.webp)

_Probe aus dem Integrationstest vom 12. November 2026: eine midjourney-Aufgabe, ein PNG mit 2048x2048 Pixeln, vier Varianten, 197,1 Sekunden von Create bis Download. Die heruntergeladenen Bytes stimmten mit dem SHA-256 der Aufgabe überein._

Die Antwort unten ist gekürzt: Die URL ist unkenntlich gemacht und `expires_at` ist historisch. Sie ergänzt die Asset-Felder, die du brauchst:

```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…"
    }
  ]
}
```

Lies `width` und `height` aus dem Asset, statt 2048 fest zu verdrahten. Das Quadrat mit 2048x2048 und die 197,1 Sekunden sind eine Probe; behandle sie so. Alle vier Varianten teilen sich ein PNG, teile das Raster nach dem Download, wenn deine App einzelne Bilder braucht.

Die `url` ist signiert und kurzlebig, etwa eine Stunde. Aufgaben-Datensatz und Bild werden laut OmniaKeys [Bild-API-Docs](https://docs.omniakey.com/en/media/image) (geprüft am 12. November 2026) 24 Stunden nach dem Anlegen aufbewahrt. Lade vor Ablauf der URL herunter. Ist sie abgelaufen, frage die Task-ID erneut ab, um eine neue signierte URL zu bekommen.

Lade die Asset-URL mit einem normalen GET und ohne `Authorization`-Header herunter. Die Signatur steckt in der URL. Das Skript vergleicht außerdem die heruntergeladenen Bytes mit dem `sha256` der Aufgabe; so fällt eine abgeschnittene Datei auf, bevor sie deine gute Kopie überschreibt.

## Nach Timeout oder fehlgeschlagenem Download erneut ausführen

Führe denselben Befehl erneut aus und behalte den Job. Das Skript liest `midjourney-task.json` und nimmt den kürzesten sicheren Weg:

- Create unbestätigt: Es sendet denselben Body mit demselben Idempotenzschlüssel per POST.
- Task-ID vorhanden und Job läuft noch: Es fragt die vorhandene ID ab.
- Aufgabe erfolgreich, aber PNG fehlt: Es fragt einmal nach einer neuen signierten URL und lädt erneut herunter.
- PNG ist gespeichert und stimmt mit dem gespeicherten SHA-256: Es beendet sich ohne weitere Anfrage. Eine beschädigte Datei wird erneut abgefragt und heruntergeladen.

Das funktioniert innerhalb der 24-Stunden-Aufbewahrung. Derselbe Schlüssel mit geändertem Prompt oder geänderten Parametern ergibt `409`. Das Skript lehnt eine Statusdatei ab, deren Prompt nicht mehr passt; verschiebe die Datei, wenn du Parameter änderst.

## Fehler und Lösungen

Die Fehler unten folgen aus den Aufgaben- und Idempotenzregeln der API. Behebe die Ursache und starte dann gegebenenfalls mit derselben Statusdatei neu.

|Antwort|Code|Was tun|
|-|-|-|
|400|`missing_idempotency_key`|Header ergänzen; unter 255 Zeichen halten|
|409|`idempotency_key_reused`|Ursprünglichen Body senden oder mit neuem Schlüssel einen neuen Job starten|
|503|`no_available_channel`|Zugriff für diesen Schlüssel/dieses Konto prüfen; bei Fortbestehen den Support kontaktieren|
|`failed`|`error.code`, `error.message`|Aufgabenfehler lesen; fehlgeschlagene Jobs werden erstattet|
|`canceled`|—|Der Job wurde gestoppt; lege einen neuen an, wenn du ihn noch brauchst|

`503 no_available_channel` belegt nicht, dass einem Schlüssel die Modellberechtigung fehlt. Zwei Einsendungen mit einem in diesem Batch verwendeten Schlüssel lieferten `503` und erzeugten keine Aufgabe; ein anderer Schlüssel war in einem früheren Integrationstest am selben Tag erfolgreich. Die Ursache ist unbekannt: Es kann die Verfügbarkeit des Schlüssels/Kontos oder des Upstream-Kanals sein. Prüfe, ob `midjourney` für deinen aktuellen Schlüssel/dein Konto verfügbar ist. Bleibt der `503`, schicke Support den Response-Body und die Request-ID.

Ein `200` beim Create ist die Wiederholung der ursprünglichen Aufgabe. Nur `failed` und `canceled` sind endgültige Fehler; stoppst du das Skript, läuft der Job weiter.

## Kosten und wann eine andere Bild-API sinnvoll ist

OmniaKey berechnet einmal pro erfolgreicher Aufgabe, fehlgeschlagene oder abgebrochene Aufgaben werden automatisch erstattet. Der aktuelle Preis steht auf der [Midjourney-Modellseite](https://omniakey.com/de/midjourney); prüfe ihn, bevor du einen Batch schätzt.

Midjourney passt, wenn du vier Varianten in einem Raster vergleichen willst. Diese Integration liefert dieses Text-zu-Bild-Raster; für ein Bild pro Aufruf oder eine Bearbeitung, die eine Ausgangsszene behält, sieh dir [GPT Image 2.5 API](https://omniakey.com/de/blog/gpt-image-2-5-api) oder [Nano Banana Pro API](https://omniakey.com/de/blog/nano-banana-pro-api) an.

Wenn du lieber einen Prompt tippst als Code schreibst, führt [AI Image Generator](https://omniakey.com/de/ai-image-generator) Bildmodelle im Browser aus.

## Häufige Fragen

### Brauche ich ein Midjourney-Abo, um diese API zu nutzen?

Nein. Der Schlüssel kommt von OmniaKey und die Aufrufe gehen an OmniaKeys Drittanbieter-API. OmniaKey ist nicht Midjourney; ein Abo auf der Midjourney-Website ist ein separates Produkt und erzeugt keinen OmniaKey-Schlüssel.

### Warum liefert eine Midjourney-Aufgabe vier Bilder in einer Datei?

Midjourney gibt ein 2x2-Raster mit vier Varianten als ein PNG zurück. Alle vier teilen diese Datei, und Upscale und Vary sind in dieser Integration nicht verfügbar; teile das Raster nach dem Download selbst, wenn du einzelne Dateien brauchst.

### Was bedeutet 409 idempotency\_key\_reused?

Du hast einen Idempotenzschlüssel gesendet, den OmniaKey schon kennt, aber der Request-Body hat sich geändert. Sende den ursprünglichen Body oder starte mit einem neuen Schlüssel einen neuen Job. Denselben Schlüssel mit einem anderen Prompt zu verwenden, ist die übliche Ursache.

### Was passiert, wenn mein Skript beim Abfragen in einen Timeout läuft?

Der Job läuft weiter. Die Task-ID steht in `midjourney-task.json`; ein erneuter Lauf desselben Skripts fragt diese ID ab oder lädt das fertige Asset herunter. Ein lokaler Timeout lässt Job und Berechnung bestehen.
