# API de Midjourney en Python: genera y guarda imágenes

> Crea una tarea de imagen de Midjourney en Python, consulta su estado y descarga la cuadrícula. Revisa qué operaciones admite la API antes de integrarla.

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

---

Este ejemplo llama desde Python a la API de [Midjourney](https://omniakey.com/es/midjourney) de OmniaKey, un servicio de terceros, en tres pasos: crear la tarea, consultar su estado hasta que termine y descargar el PNG desde la URL firmada del recurso. Una prueba de integración del 12 de noviembre de 2026 devolvió una imagen de 2048x2048 en 197,1 segundos, y esa imagen es una cuadrícula 2x2 con cuatro candidatos. El script de abajo añade un archivo de estado, así que un tiempo de espera agotado o una descarga fallida no crean un segundo trabajo de pago.

## Respuesta corta

- Envía `POST https://api.omniakey.com/v1/media/tasks` con un `Idempotency-Key`, luego consulta `GET /v1/media/tasks/{id}` y descarga `assets[0].url`.
- Una tarea devuelve una cuadrícula PNG con cuatro imágenes, no cuatro archivos separados.
- Midjourney en esta API solo hace texto a imagen: sin edición, imagen de referencia, blend, upscale, vary, desplazamiento ni selector de versión.
- La clave viene de OmniaKey y es independiente de una suscripción de Midjourney. Guarda el cuerpo y la clave de idempotencia antes del POST, añade el id de la tarea cuando la API la acepte y conserva el archivo de estado para reanudar más tarde. Un tiempo de espera local no cancela la tarea del servidor.

## ¿Qué API usa este ejemplo de Python?

El ejemplo llama a la API de terceros de OmniaKey para Midjourney. OmniaKey no es Midjourney, y una clave de OmniaKey es una cuenta aparte con su propio saldo. Una suscripción del sitio web de Midjourney es un producto distinto y no crea una clave de OmniaKey.

La [página del modelo Midjourney](https://omniakey.com/es/midjourney) muestra el modelo y la tarifa actual del lado de OmniaKey. El resto del recorrido cubre esta integración y el ciclo de vida de sus tareas.

## Qué necesitas

Necesitas tres cosas:

1. Una clave de API de OmniaKey, exportada como `OMNIAKEY_API_KEY`.
2. Python 3. El script usa solo la biblioteca estándar, así que no hay SDK que instalar.
3. `midjourney` disponible para esa clave/cuenta. Si el modelo no aparece en la lista de la clave, revisa el acceso de tu cuenta antes de ejecutar la llamada de creación.

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

Mantén la clave fuera del script y de los registros. El código la lee una vez del entorno.

## Cuerpo de la solicitud y parámetros

Midjourney recibe un prompt y tres parámetros. El tipo de bot está preconfigurado en OmniaKey, así que el cuerpo puede omitir la versión del modelo y `botType`.

|Campo|Valor|Notas|
|-|-|-|
|`input.prompt`|Texto|La escena que quieres|
|`parameters.aspectRatio`|`1:1`|También `16:9`, `9:16`, `3:2`, `2:3`|
|`parameters.quality`|`"1"`|La documentación usa la cadena `"1"`|
|`parameters.n`|`1`|Debe ser 1|

Define la forma con `aspectRatio`, no con un tamaño en píxeles. El campo va en camelCase: `aspectRatio`, nunca `aspect_ratio`. Usa uno de los cinco valores indicados.

El cuerpo para el prompt de muestra es así:

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

## Llamar a la API de Midjourney desde Python

Guarda esto como `midjourney_grid.py` y ejecútalo. La primera ejecución escribe `midjourney-task.json` antes del POST y añade el id de la tarea cuando OmniaKey acepta la solicitud.

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

Las formas de la solicitud y la respuesta vienen de la prueba de integración del 12 de noviembre; el estado de reinicio es la novedad aquí, y sus pasos del ciclo de vida se comprobaron sin conexión con respuestas simuladas.

El archivo de estado conserva lo necesario para reanudar: el cuerpo JSON y el UUID usado como clave de idempotencia. No guarda ninguna clave de API. Se escribe antes del POST, así que una conexión interrumpida deja ambos valores en disco para recuperarlos.

La llamada de creación puede devolver dos códigos de éxito. Un `202` acepta un trabajo nuevo. Un `200` significa que OmniaKey ya vio la misma clave y el mismo cuerpo y devuelve la tarea original; sigue consultando esa tarea. Vuelve a enviar el mismo cuerpo y la misma clave solo cuando el resultado de la creación sea desconocido tras un fallo de conexión. La documentación indica `409 idempotency_key_reused` para un cuerpo modificado.

Controla el bucle de consulta con `status`. El porcentaje es orientativo y puede quedarse en 0 mientras el trabajo avanza. Detente en `succeeded`, `failed` o `canceled`. La [documentación del ciclo de vida de tareas multimedia](https://docs.omniakey.com/es/media/overview) de OmniaKey (revisada el 12 de noviembre de 2026) describe esos estados.

El límite de consultas es local. Cuando vence, el trabajo sigue en ejecución o en cola; al volver a ejecutar el mismo script se lee el id guardado y se sigue consultando. Un tiempo de espera local no es una cancelación. La cancelación se hace con otro endpoint.

## La cuadrícula de imágenes de Midjourney que devuelve la API

Una tarea exitosa tiene un solo recurso: una cuadrícula PNG. Esta es la muestra de la prueba de integración del 12 de noviembre de 2026:

![Cuatro barcos de papel rojos plegados sobre agua azul al amanecer, ordenados en una cuadrícula 2x2 con luz cálida reflejada](https://omniakey.com/_img/blog/midjourney-api-python/paper-boat-grid.03735622.1600.webp)

_Muestra de la prueba de integración del 12 de noviembre de 2026: una tarea midjourney, un PNG de 2048x2048, cuatro candidatos, 197,1 segundos desde la creación hasta la descarga. Los bytes descargados coincidieron con el SHA-256 de la tarea._

La respuesta de abajo está abreviada: la URL está oculta y `expires_at` es histórico. Añade los campos del recurso que necesitas:

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

Lee `width` y `height` del recurso en lugar de fijar 2048. El cuadrado de 2048x2048 y los 197,1 segundos son una muestra; trátalos como tal. Los cuatro candidatos comparten un PNG, así que divide la cuadrícula tras la descarga si tu aplicación necesita imágenes sueltas.

La `url` está firmada y dura poco, alrededor de una hora. El registro de la tarea y la imagen se conservan 24 horas después de la creación, según la [documentación de la API de imágenes](https://docs.omniakey.com/es/media/image) de OmniaKey (revisada el 12 de noviembre de 2026). Descarga antes de que caduque la URL. Si ya caducó, consulta otra vez el id de la tarea para obtener una URL firmada nueva.

Descarga la URL del recurso con un GET normal y sin cabecera `Authorization`. La firma está en la URL. El script también compara los bytes descargados con el `sha256` de la tarea, lo que detecta un archivo truncado antes de que sobrescribas tu copia buena.

## Volver a ejecutar tras un tiempo de espera o una descarga fallida

Ejecuta el mismo comando y conserva el trabajo. El script lee `midjourney-task.json` y toma el camino seguro más corto:

- La creación no se confirmó: hace POST del mismo cuerpo con la misma clave de idempotencia.
- Existe el id y el trabajo sigue en ejecución: consulta el id existente.
- El trabajo terminó pero falta el PNG: consulta una vez para obtener una URL firmada nueva y descarga de nuevo.
- El PNG ya está guardado y coincide con su SHA-256 registrado: sale sin otra solicitud. Un archivo dañado se consulta y se descarga otra vez.

Esto funciona dentro de la ventana de conservación de 24 horas. La misma clave con un prompt o unos parámetros distintos produce un `409`. El script rechaza un archivo de estado cuyo prompt ya no coincide; mueve el archivo si cambias los parámetros.

## Errores y soluciones

Los fallos de abajo vienen de las reglas de tareas e idempotencia de la API. Corrige la causa y vuelve a ejecutar con el mismo archivo de estado cuando corresponda.

|Respuesta|Código|Qué hacer|
|-|-|-|
|400|`missing_idempotency_key`|Añade la cabecera; mantenla por debajo de 255 caracteres|
|409|`idempotency_key_reused`|Envía el cuerpo original o empieza un trabajo nuevo con otra clave|
|503|`no_available_channel`|Comprueba el acceso de esta clave/cuenta; contacta con soporte si persiste|
|`failed`|`error.code`, `error.message`|Lee el error de la tarea; los trabajos fallidos se reembolsan|
|`canceled`|—|El trabajo se detuvo; crea otro si aún lo necesitas|

Un `503 no_available_channel` no demuestra que a una clave le falte permiso para el modelo. Dos envíos con una clave usada en este lote devolvieron `503` y no crearon ninguna tarea; otra clave tuvo éxito en una prueba de integración anterior del mismo día. La causa se desconoce: puede ser la disponibilidad de la clave/cuenta o la del canal upstream. Confirma que `midjourney` está disponible para tu clave/cuenta actual. Si el `503` persiste, envía a soporte el cuerpo de la respuesta y el id de la solicitud.

Un `200` al crear es una repetición de la tarea original. Solo `failed` y `canceled` son fallos terminales; si detienes el script, el trabajo sigue.

## Coste y cuándo usar otra API de imágenes

OmniaKey cobra una vez por trabajo exitoso, y los trabajos fallidos o cancelados se reembolsan automáticamente. La tarifa actual está en la [página del modelo Midjourney](https://omniakey.com/es/midjourney); revísala antes de calcular un lote.

Midjourney encaja para elegir entre cuatro variaciones en una cuadrícula. Esta integración devuelve esa cuadrícula de texto a imagen; para una imagen por llamada o una edición que conserve una escena de origen, consulta [GPT Image 2.5 API](https://omniakey.com/es/blog/gpt-image-2-5-api) o [Nano Banana Pro API](https://omniakey.com/es/blog/nano-banana-pro-api).

Si prefieres escribir un prompt antes que código, [AI Image Generator](https://omniakey.com/es/ai-image-generator) ejecuta modelos de imagen en el navegador.

## Preguntas frecuentes

### ¿Necesito una suscripción de Midjourney para usar esta API?

No. La clave viene de OmniaKey y las llamadas van a la API de terceros de OmniaKey. OmniaKey no es Midjourney; una suscripción del sitio web de Midjourney es un producto distinto y no crea una clave de OmniaKey.

### ¿Por qué una tarea de Midjourney devuelve cuatro imágenes en un solo archivo?

Midjourney devuelve una cuadrícula 2x2 con cuatro candidatos como un único PNG. Los cuatro comparten ese archivo, y en esta integración no hay upscale ni vary, así que divide la cuadrícula tú mismo tras la descarga si necesitas archivos separados.

### ¿Qué significa 409 idempotency\_key\_reused?

Enviaste una clave de idempotencia que OmniaKey ya tiene, pero el cuerpo de la solicitud cambió. Envía el cuerpo original o empieza un trabajo nuevo con otra clave. Reutilizar una clave con un prompt distinto es la causa habitual.

### ¿Qué pasa si mi script agota el tiempo mientras consulta?

El trabajo sigue en ejecución. El id de la tarea está en `midjourney-task.json`, así que al volver a ejecutar el mismo script consultará ese id o descargará el recurso terminado. Un tiempo de espera local deja vivo el trabajo y mantiene el cargo.
