# API Midjourney en Python : générer et enregistrer

> Créer une tâche Midjourney en Python, suivre son état et télécharger la grille d'images. Les opérations disponibles avant de coder votre intégration.

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

---

Cet exemple appelle depuis Python l'API [Midjourney](https://omniakey.com/fr/midjourney) d'OmniaKey, un service tiers, en trois étapes : créer la tâche, interroger son état jusqu'à la fin, puis télécharger le PNG depuis l'URL signée de l'actif. Un test d'intégration du 12 novembre 2026 a renvoyé une image 2048x2048 en 197,1 secondes, et cette image est une grille 2x2 de quatre candidats. Le script ci-dessous ajoute un fichier d'état : un délai dépassé ou un téléchargement échoué ne crée donc pas une deuxième tâche payante.

## Réponse courte

- Envoyez `POST https://api.omniakey.com/v1/media/tasks` avec un `Idempotency-Key`, puis interrogez `GET /v1/media/tasks/{id}` et téléchargez `assets[0].url`.
- Une tâche renvoie une grille PNG de quatre images, pas quatre fichiers séparés.
- Midjourney sur cette API fait uniquement du texte vers image : pas d'édition, d'image de référence, de blend, d'upscale, de vary, de décalage ni de sélecteur de version.
- La clé vient d'OmniaKey et est distincte d'un abonnement Midjourney. Enregistrez le corps et la clé d'idempotence avant le POST, ajoutez l'id de tâche quand l'API accepte la requête, et gardez le fichier d'état pour reprendre plus tard. Un délai local n'annule pas la tâche côté serveur.

## Quelle API utilise cet exemple Python ?

L'exemple appelle l'API tierce d'OmniaKey pour Midjourney. OmniaKey n'est pas Midjourney, et une clé OmniaKey correspond à un compte distinct avec son propre solde. Un abonnement au site Midjourney est un autre produit et ne crée pas de clé OmniaKey.

La [page du modèle Midjourney](https://omniakey.com/fr/midjourney) affiche le modèle et le tarif actuel côté OmniaKey. La suite présente cette intégration et le déroulement de ses tâches.

## Ce qu'il vous faut

Trois éléments :

1. Une clé API OmniaKey, exportée sous `OMNIAKEY_API_KEY`.
2. Python 3. Le script n'utilise que la bibliothèque standard, aucun SDK à installer.
3. `midjourney` disponible pour cette clé/cet compte. Si le modèle manque dans la liste de la clé, vérifiez l'accès du compte avant de lancer l'appel de création.

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

Gardez la clé hors du script et des journaux. Le code la lit une fois depuis l'environnement.

## Corps de la requête et paramètres

Midjourney reçoit un prompt et trois paramètres. Le type de bot est préréglé côté OmniaKey, le corps peut donc omettre la version du modèle et `botType`.

|Champ|Valeur|Remarques|
|-|-|-|
|`input.prompt`|Texte|La scène voulue|
|`parameters.aspectRatio`|`1:1`|Aussi `16:9`, `9:16`, `3:2`, `2:3`|
|`parameters.quality`|`"1"`|La documentation utilise la chaîne `"1"`|
|`parameters.n`|`1`|Doit valoir 1|

Définissez la forme avec `aspectRatio`, pas avec une taille en pixels. Le champ est en camelCase : `aspectRatio`, jamais `aspect_ratio`. Utilisez l'une des cinq valeurs listées.

Le corps pour le prompt d'exemple ressemble à ceci :

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

## Appeler l'API Midjourney en Python

Enregistrez-le sous `midjourney_grid.py` et exécutez-le. Le premier lancement écrit `midjourney-task.json` avant le POST, puis ajoute l'id de tâche quand OmniaKey accepte la requête.

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

Les formes de la requête et de la réponse viennent du test d'intégration du 12 novembre ; l'état de reprise est l'ajout ici, et ses branches ont été vérifiées hors ligne avec des réponses simulées.

Le fichier d'état conserve ce qu'il faut pour reprendre : le corps JSON et l'UUID utilisé comme clé d'idempotence. Il ne contient aucune clé API. Il est écrit avant le POST, donc une connexion coupée laisse les deux valeurs sur disque pour la reprise.

L'appel de création peut renvoyer deux codes de succès. Un `202` accepte une nouvelle tâche. Un `200` signifie qu'OmniaKey a déjà vu la même clé et le même corps et renvoie la tâche d'origine ; continuez à interroger cette tâche. Ne renvoyez le même corps et la même clé que si le résultat de la création est inconnu après une coupure de connexion. La documentation indique `409 idempotency_key_reused` pour un corps modifié.

Pilotez la boucle d'interrogation avec `status`. Le pourcentage est indicatif et peut rester à 0 pendant l'exécution. Arrêtez-vous sur `succeeded`, `failed` ou `canceled`. La [documentation du cycle de vie des tâches média](https://docs.omniakey.com/en/media/overview) d'OmniaKey (vérifiée le 12 novembre 2026) décrit ces états.

La limite d'interrogation est locale. Quand elle expire, la tâche tourne ou attend encore ; relancer le même script lit l'id enregistré et continue l'interrogation. Un délai local n'est pas une annulation. L'annulation passe par un autre endpoint.

## La grille d'images Midjourney renvoyée

Une tâche réussie a un seul actif : une grille PNG. Voici l'échantillon du test d'intégration du 12 novembre 2026 :

![Quatre bateaux en papier rouges pliés sur une eau bleue au lever du soleil, disposés en grille 2x2 avec une lumière chaude réfléchie](https://omniakey.com/_img/blog/midjourney-api-python/paper-boat-grid.03735622.1600.webp)

_Échantillon du test d'intégration du 12 novembre 2026 : une tâche midjourney, un PNG 2048x2048, quatre candidats, 197,1 secondes entre la création et le téléchargement. Les octets téléchargés correspondaient au SHA-256 de la tâche._

La réponse ci-dessous est abrégée : l'URL est masquée et `expires_at` est historique. Elle ajoute les champs d'actif utiles :

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

Lisez `width` et `height` depuis l'actif au lieu de coder 2048 en dur. Le carré 2048x2048 et les 197,1 secondes sont un échantillon ; traitez-les comme tel. Les quatre candidats partagent un PNG, découpez donc la grille après le téléchargement si votre application veut des images séparées.

L'`url` est signée et de courte durée, environ une heure. L'enregistrement de la tâche et l'image sont conservés 24 heures après la création, selon la [documentation de l'API d'images](https://docs.omniakey.com/en/media/image) d'OmniaKey (vérifiée le 12 novembre 2026). Téléchargez avant l'expiration de l'URL. Si elle a expiré, interrogez de nouveau l'id de tâche pour obtenir une URL signée fraîche.

Téléchargez l'URL de l'actif avec un GET simple et sans en-tête `Authorization`. La signature est dans l'URL. Le script compare aussi les octets téléchargés au `sha256` de la tâche, ce qui détecte un fichier tronqué avant d'écraser votre bonne copie.

## Relancer après un délai ou un téléchargement échoué

Relancez la même commande et gardez la tâche. Le script lit `midjourney-task.json` et prend le chemin sûr le plus court :

- Création non confirmée : il refait un POST du même corps avec la même clé d'idempotence.
- L'id existe et la tâche tourne encore : il interroge l'id existant.
- La tâche a réussi mais le PNG manque : il interroge une fois pour une URL signée fraîche et télécharge de nouveau.
- Le PNG est déjà enregistré et correspond à son SHA-256 : il sort sans autre requête. Un fichier corrompu est interrogé et téléchargé de nouveau.

Cela fonctionne dans la fenêtre de conservation de 24 heures. La même clé avec un prompt ou des paramètres différents donne un `409`. Le script refuse un fichier d'état dont le prompt ne correspond plus ; déplacez le fichier si vous changez les paramètres.

## Erreurs et solutions

Les échecs ci-dessous découlent des règles de tâche et d'idempotence de l'API. Corrigez la cause, puis relancez avec le même fichier d'état le cas échéant.

|Réponse|Code|Que faire|
|-|-|-|
|400|`missing_idempotency_key`|Ajoutez l'en-tête ; gardez-le sous 255 caractères|
|409|`idempotency_key_reused`|Renvoyez le corps d'origine ou démarrez une nouvelle tâche avec une nouvelle clé|
|503|`no_available_channel`|Vérifiez l'accès de cette clé/cet compte ; contactez le support si cela persiste|
|`failed`|`error.code`, `error.message`|Lisez l'erreur de la tâche ; les tâches échouées sont remboursées|
|`canceled`|—|La tâche s'est arrêtée ; créez-en une autre si vous en avez encore besoin|

Un `503 no_available_channel` ne prouve pas qu'une clé n'a pas la permission du modèle. Deux envois avec une clé utilisée pour ce lot ont renvoyé `503` et n'ont créé aucune tâche ; une autre clé a réussi lors d'un test d'intégration antérieur le même jour. La cause est inconnue : il peut s'agir de la disponibilité de la clé/du compte ou du canal en amont. Vérifiez que `midjourney` est disponible pour votre clé/votre compte actuel. Si le `503` persiste, envoyez au support le corps de la réponse et l'id de requête.

Un `200` à la création est un rejeu de la tâche d'origine. Seuls `failed` et `canceled` sont des échecs terminaux ; arrêter le script laisse la tâche tourner.

## Coût et quand choisir une autre API d'images

OmniaKey facture une fois par tâche réussie, et les tâches échouées ou annulées sont remboursées automatiquement. Le tarif actuel figure sur la [page du modèle Midjourney](https://omniakey.com/fr/midjourney) ; vérifiez-le avant d'estimer un lot.

Midjourney convient pour choisir entre quatre variantes dans une grille. Cette intégration renvoie cette grille texte vers image ; pour une image par appel ou une édition qui conserve une scène source, voyez [GPT Image 2.5 API](https://omniakey.com/fr/blog/gpt-image-2-5-api) ou [Nano Banana Pro API](https://omniakey.com/fr/blog/nano-banana-pro-api).

Si vous préférez saisir un prompt plutôt qu'écrire du code, [AI Image Generator](https://omniakey.com/fr/ai-image-generator) exécute des modèles d'image dans le navigateur.

## Questions fréquentes

### Faut-il un abonnement Midjourney pour utiliser cette API ?

Non. La clé vient d'OmniaKey et les appels vont à l'API tierce d'OmniaKey. OmniaKey n'est pas Midjourney ; un abonnement au site Midjourney est un autre produit et ne crée pas de clé OmniaKey.

### Pourquoi une tâche Midjourney renvoie-t-elle quatre images dans un seul fichier ?

Midjourney renvoie une grille 2x2 de quatre candidats sous forme d'un seul PNG. Les quatre partagent ce fichier, et l'upscale et le vary ne sont pas disponibles sur cette intégration ; découpez donc la grille vous-même après le téléchargement si vous voulez des fichiers séparés.

### Que signifie 409 idempotency\_key\_reused ?

Vous avez envoyé une clé d'idempotence qu'OmniaKey connaît déjà, mais le corps de la requête a changé. Renvoyez le corps d'origine ou démarrez une nouvelle tâche avec une nouvelle clé. Réutiliser une clé avec un prompt différent est la cause habituelle.

### Que se passe-t-il si mon script expire pendant l'interrogation ?

La tâche continue. L'id de tâche est dans `midjourney-task.json` ; relancer le même script interrogera cet id ou téléchargera l'actif terminé. Un délai local laisse la tâche en vie et la facturation en place.
