# API do Midjourney em Python: gerar e salvar imagens

> Crie uma tarefa de imagem do Midjourney em Python, consulte o status e baixe a grade. Veja quais operações a API oferece antes de integrar.

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

---

Este exemplo usa Python para chamar a API do [Midjourney](https://omniakey.com/pt-BR/midjourney) da OmniaKey, um serviço de terceiros, em três etapas: criar a tarefa, consultar o status até concluir e baixar o PNG da URL assinada do arquivo. Um teste de integração em 12 de novembro de 2026 retornou uma imagem 2048x2048 em 197,1 segundos, e essa imagem é uma grade 2x2 com quatro candidatos. O script abaixo adiciona um arquivo de estado, então um tempo esgotado ou um download com falha não cria uma segunda tarefa paga.

## Resposta curta

- Envie `POST https://api.omniakey.com/v1/media/tasks` com um `Idempotency-Key`, depois consulte `GET /v1/media/tasks/{id}` e baixe `assets[0].url`.
- Uma tarefa retorna uma grade PNG com quatro imagens, não quatro arquivos separados.
- O Midjourney nesta API faz só texto para imagem: sem edição, imagem de referência, blend, upscale, vary, deslocamento ou seletor de versão.
- A chave vem da OmniaKey e é separada de uma assinatura do Midjourney. Salve o corpo e a chave de idempotência antes do POST, adicione o id da tarefa quando a API aceitar e mantenha o arquivo de estado para retomar depois. Um tempo esgotado local não cancela a tarefa no servidor.

## Qual API este exemplo de Python usa?

O exemplo chama a API de terceiros da OmniaKey para o Midjourney. A OmniaKey não é o Midjourney, e uma chave OmniaKey é uma conta separada com saldo próprio. Uma assinatura no site do Midjourney é outro produto e não cria uma chave OmniaKey.

A [página do modelo Midjourney](https://omniakey.com/pt-BR/midjourney) mostra o modelo e a tarifa atual do lado da OmniaKey. O restante do passo a passo cobre esta integração e o ciclo de vida das tarefas.

## O que você precisa

Você precisa de três coisas:

1. Uma chave de API da OmniaKey, exportada como `OMNIAKEY_API_KEY`.
2. Python 3. O script usa só a biblioteca padrão, então não há SDK para instalar.
3. `midjourney` disponível para essa chave/conta. Se o modelo não aparecer na lista da chave, confira o acesso da conta antes de executar a chamada de criação.

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

Mantenha a chave fora do script e dos logs. O código a lê uma vez do ambiente.

## Corpo da requisição e parâmetros

O Midjourney recebe um prompt e três parâmetros. O tipo de bot é predefinido do lado da OmniaKey, então o corpo pode omitir a versão do modelo e `botType`.

|Campo|Valor|Observações|
|-|-|-|
|`input.prompt`|Texto|A cena desejada|
|`parameters.aspectRatio`|`1:1`|Também `16:9`, `9:16`, `3:2`, `2:3`|
|`parameters.quality`|`"1"`|A documentação usa a string `"1"`|
|`parameters.n`|`1`|Precisa ser 1|

Defina o formato com `aspectRatio`, não com um tamanho em pixels. O campo é camelCase: `aspectRatio`, nunca `aspect_ratio`. Use um dos cinco valores listados.

O corpo para o prompt de exemplo fica assim:

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

## Chamar a API do Midjourney em Python

Salve como `midjourney_grid.py` e execute. A primeira execução grava `midjourney-task.json` antes do POST e adiciona o id da tarefa quando a OmniaKey aceita a requisição.

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

As formas da requisição e da resposta vêm do teste de integração de 12 de novembro; o estado de reinício é a adição aqui, e seus caminhos de ciclo de vida foram verificados offline com respostas simuladas.

O arquivo de estado guarda o que você precisa para continuar: o corpo JSON e o UUID usado como chave de idempotência. Não guarda nenhuma chave de API. É gravado antes do POST, então uma conexão interrompida deixa os dois valores no disco para recuperação.

A chamada de criação pode retornar dois códigos de sucesso. Um `202` aceita uma tarefa nova. Um `200` significa que a OmniaKey já viu a mesma chave e o mesmo corpo e está devolvendo a tarefa original; continue consultando essa tarefa. Envie o mesmo corpo e a chave de novo só quando o resultado da criação for desconhecido após uma falha de conexão. A documentação lista `409 idempotency_key_reused` para um corpo alterado.

Controle o loop de consulta por `status`. A porcentagem é aproximada e pode ficar em 0 enquanto a tarefa roda. Pare em `succeeded`, `failed` ou `canceled`. A [documentação do ciclo de vida de tarefas de mídia](https://docs.omniakey.com/en/media/overview) da OmniaKey (verificada em 12 de novembro de 2026) descreve esses estados.

O limite de consultas é local. Quando ele vence, a tarefa ainda está rodando ou na fila; executar o mesmo script de novo lê o id salvo e continua a consulta. Um tempo esgotado local não é cancelamento. O cancelamento usa outro endpoint.

## A grade de imagens do Midjourney retornada

Uma tarefa concluída tem um único arquivo: uma grade PNG. Esta é a amostra do teste de integração de 12 de novembro de 2026:

![Quatro barcos de papel vermelhos dobrados sobre água azul ao nascer do sol, dispostos em uma grade 2x2 com luz quente refletida](https://omniakey.com/_img/blog/midjourney-api-python/paper-boat-grid.03735622.1600.webp)

_Amostra do teste de integração de 12 de novembro de 2026: uma tarefa midjourney, um PNG 2048x2048, quatro candidatos, 197,1 segundos da criação ao download. Os bytes baixados corresponderam ao SHA-256 da tarefa._

A resposta abaixo está resumida: a URL está oculta e `expires_at` é histórico. Ela adiciona os campos de arquivo necessários:

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

Leia `width` e `height` do arquivo em vez de fixar 2048. O quadrado 2048x2048 e os 197,1 segundos são uma amostra; trate-os assim. Os quatro candidatos compartilham um PNG, então divida a grade após o download se o app precisar de imagens separadas.

A `url` é assinada e dura pouco, cerca de uma hora. O registro da tarefa e a imagem ficam guardados por 24 horas após a criação, segundo a [documentação da API de imagens](https://docs.omniakey.com/en/media/image) da OmniaKey (verificada em 12 de novembro de 2026). Baixe antes de a URL expirar. Se ela expirou, consulte o id da tarefa de novo para obter uma URL assinada nova.

Baixe a URL do arquivo com um GET comum e sem cabeçalho `Authorization`. A assinatura está na URL. O script também compara os bytes baixados com o `sha256` da tarefa, o que detecta um arquivo truncado antes de sobrescrever sua cópia boa.

## Executar de novo após tempo esgotado ou download com falha

Execute o mesmo comando e mantenha a tarefa. O script lê `midjourney-task.json` e escolhe o caminho seguro mais curto:

- Criação não confirmada: ele faz POST do mesmo corpo com a mesma chave de idempotência.
- O id existe e a tarefa ainda roda: ele consulta o id existente.
- A tarefa concluiu mas o PNG não está lá: ele consulta uma vez para obter uma URL assinada nova e baixa de novo.
- O PNG já está salvo e corresponde ao SHA-256 registrado: ele sai sem outra requisição. Um arquivo corrompido é consultado e baixado de novo.

Isso funciona dentro da janela de retenção de 24 horas. A mesma chave com um prompt ou parâmetros diferentes gera `409`. O script recusa um arquivo de estado cujo prompt não corresponde mais; mova o arquivo se mudar os parâmetros.

## Erros e correções

As falhas abaixo vêm das regras de tarefa e idempotência da API. Corrija a causa e execute de novo com o mesmo arquivo de estado quando for o caso.

|Resposta|Código|O que fazer|
|-|-|-|
|400|`missing_idempotency_key`|Adicione o cabeçalho; mantenha-o com menos de 255 caracteres|
|409|`idempotency_key_reused`|Envie o corpo original ou comece uma tarefa nova com outra chave|
|503|`no_available_channel`|Confira o acesso desta chave/conta; fale com o suporte se persistir|
|`failed`|`error.code`, `error.message`|Leia o erro da tarefa; tarefas com falha são reembolsadas|
|`canceled`|—|A tarefa parou; crie outra se ainda precisar|

Um `503 no_available_channel` não prova que uma chave não tem permissão para o modelo. Dois envios com uma chave usada neste lote retornaram `503` e não criaram tarefa; outra chave teve sucesso em um teste de integração anterior no mesmo dia. A causa é desconhecida: pode ser a disponibilidade da chave/conta ou do canal upstream. Confirme que `midjourney` está disponível para sua chave/conta atual. Se o `503` persistir, envie ao suporte o corpo da resposta e o id da requisição.

Um `200` na criação é a repetição da tarefa original. Só `failed` e `canceled` são falhas terminais; parar o script deixa a tarefa rodando.

## Custo e quando usar outra API de imagens

A OmniaKey cobra uma vez por tarefa concluída, e tarefas com falha ou canceladas são reembolsadas automaticamente. A tarifa atual está na [página do modelo Midjourney](https://omniakey.com/pt-BR/midjourney); confira antes de estimar um lote.

O Midjourney serve para escolher entre quatro variações em uma grade. Esta integração retorna essa grade de texto para imagem; para uma imagem por chamada ou uma edição que preserve uma cena de origem, veja [GPT Image 2.5 API](https://omniakey.com/pt-BR/blog/gpt-image-2-5-api) ou [Nano Banana Pro API](https://omniakey.com/pt-BR/blog/nano-banana-pro-api).

Se preferir digitar um prompt a escrever código, o [AI Image Generator](https://omniakey.com/pt-BR/ai-image-generator) executa modelos de imagem no navegador.

## Perguntas frequentes

### Preciso de uma assinatura do Midjourney para usar esta API?

Não. A chave vem da OmniaKey e as chamadas vão para a API de terceiros da OmniaKey. A OmniaKey não é o Midjourney; uma assinatura no site do Midjourney é outro produto e não cria uma chave OmniaKey.

### Por que uma tarefa do Midjourney retorna quatro imagens em um único arquivo?

O Midjourney retorna uma grade 2x2 com quatro candidatos como um único PNG. Os quatro compartilham esse arquivo, e upscale e vary não estão disponíveis nesta integração, então divida a grade você mesmo após o download se precisar de arquivos separados.

### O que significa 409 idempotency\_key\_reused?

Você enviou uma chave de idempotência que a OmniaKey já tem, mas o corpo da requisição mudou. Envie o corpo original ou comece uma tarefa nova com outra chave. Reutilizar uma chave com um prompt diferente é a causa comum.

### O que acontece se meu script atingir o tempo limite durante a consulta?

A tarefa continua rodando. O id da tarefa está em `midjourney-task.json`, então executar o mesmo script de novo vai consultar esse id ou baixar o arquivo concluído. Um tempo esgotado local mantém a tarefa viva e a cobrança em vigor.
