Midjourney API Python 사용법: 이미지 생성과 저장
작업을 만들고 완료될 때까지 폴링한 뒤 2x2 PNG 그리드 한 장을 저장합니다. POST 전에 키와 요청 본문을 디스크에 기록합니다.
이 페이지 목차
2026년 11월 12일 연동 테스트에서 OmniaKey의 서드파티 Midjourney API로 만든 작업 하나가 197.1초 만에 2048x2048 이미지를 반환했습니다. 그 이미지는 후보 네 개를 담은 2x2 그리드였습니다. 이 튜토리얼의 Python 예제는 작업 생성, 상태 폴링, 서명된 애셋 URL에서 PNG 다운로드 순서로 진행합니다. 아래 스크립트는 상태 파일을 추가하므로 시간 초과나 다운로드 실패 뒤에 다시 실행해도 유료 작업이 두 번 생기지 않습니다.
짧은 답변
POST https://api.omniakey.com/v1/media/tasks에Idempotency-Key를 붙여 보내고,GET /v1/media/tasks/{id}를 폴링한 뒤assets[0].url을 내려받습니다.- 작업 하나는 이미지 네 개를 담은 PNG 그리드 한 장을 반환합니다. 파일 네 개가 아닙니다.
- 이 API의 Midjourney는 텍스트→이미지만 지원합니다. 편집, 참조 이미지, blend, 업스케일, vary, 패닝, 버전 선택은 없습니다.
- 키는 OmniaKey에서 발급하며 Midjourney 구독과 별개입니다. POST 전에 요청 본문과 멱등성 키를 저장하고, API가 요청을 받아들이면 작업 ID를 추가합니다. 상태 파일을 남겨 두면 나중에 이어서 할 수 있습니다. 로컬 시간 초과로 서버 작업이 취소되지는 않습니다.
이 Python 예제는 어떤 API를 쓰나요?
예제는 OmniaKey가 제공하는 Midjourney용 서드파티 API를 호출합니다. OmniaKey는 Midjourney가 아니며, OmniaKey 키는 자체 잔액을 가진 별도 계정입니다. Midjourney 웹사이트 구독은 별도 제품이고 OmniaKey 키를 만들지 않습니다.
Midjourney 모델 페이지에서 OmniaKey 쪽 모델과 현재 요금을 볼 수 있습니다. 아래에서는 이 연동과 작업 흐름을 다룹니다.
필요한 것
세 가지가 필요합니다.
- OmniaKey API 키. 환경 변수
OMNIAKEY_API_KEY에 설정합니다. - Python 3. 스크립트는 표준 라이브러리만 쓰므로 SDK를 설치할 필요가 없습니다.
- 해당 키/계정에서
midjourney를 쓸 수 있어야 합니다. 키의 모델 목록에 없다면 생성 요청을 보내기 전에 계정 접근 권한을 확인하세요.
export OMNIAKEY_API_KEY="your-key"
python3 midjourney_grid.py
키는 스크립트나 로그에 남기지 마세요. 코드는 환경 변수에서 한 번만 읽습니다.
요청 본문과 파라미터
Midjourney에는 프롬프트와 파라미터 세 개를 보냅니다. bot type은 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로 지정합니다. 필드는 카멜 표기법이며 aspect_ratio가 아니라 aspectRatio입니다. 위에 나온 다섯 값 중 하나를 쓰세요.
샘플 프롬프트의 본문은 다음과 같습니다.
{
"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}
}
Python에서 Midjourney API 호출하기
midjourney_grid.py로 저장하고 실행하세요. 처음 실행하면 POST 전에 midjourney-task.json을 쓰고, OmniaKey가 요청을 받아들이면 작업 ID를 추가합니다.
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()
요청과 응답 형태는 11월 12일 연동 테스트에서 가져왔습니다. 재시작 상태 관리는 여기서 추가한 부분이며, 수명 주기 경로는 모의 응답으로 오프라인에서 확인했습니다.
상태 파일에는 이어서 실행하는 데 필요한 것이 남습니다. JSON 본문과 멱등성 키로 쓰는 UUID입니다. API 키는 저장하지 않습니다. POST 전에 기록하므로 연결이 끊겨도 복구에 필요한 두 값이 디스크에 남습니다.
생성 호출은 성공 코드 두 개를 반환할 수 있습니다. 202는 새 작업을 받아들였다는 뜻입니다. 200은 OmniaKey가 같은 키와 같은 본문을 전에 본 적이 있어 원래 작업을 돌려준다는 뜻입니다. 그 작업을 계속 폴링하세요. 같은 본문과 키를 다시 보내는 것은 연결 장애 뒤 생성 결과를 알 수 없을 때뿐입니다. 본문을 바꾸면 409 idempotency_key_reused가 됩니다.
폴링은 status로 제어합니다. 퍼센트는 참고값이라 작업이 도는 동안 0에 머물 수 있습니다. succeeded, failed, canceled에서 멈춥니다. OmniaKey의 미디어 작업 수명 주기 문서(2026년 11월 12일 확인)에 이 상태들이 나와 있습니다.
폴링 상한은 로컬 설정입니다. 상한에 닿아도 작업은 계속 실행 중이거나 대기 중이며, 같은 스크립트를 다시 실행하면 저장된 작업 ID를 읽어 폴링을 이어 갑니다. 로컬 시간 초과는 취소가 아닙니다. 취소는 별도 엔드포인트로 합니다.
돌아오는 Midjourney 이미지 그리드
성공한 작업에는 애셋이 하나뿐입니다. PNG 그리드입니다. 아래는 2026년 11월 12일 연동 테스트에서 받은 샘플입니다.

아래 응답은 줄인 버전입니다. URL은 가렸고 expires_at은 당시 값입니다. 필요한 애셋 필드를 덧붙였습니다.
{
"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…"
}
]
}
2048을 고정값으로 두지 말고 width와 height를 애셋에서 읽으세요. 2048x2048과 197.1초는 샘플 하나일 뿐입니다. 후보 네 개가 PNG 한 장을 공유하므로 낱장 이미지가 필요하면 다운로드 후 그리드를 나누세요.
url은 서명되어 있고 약 1시간만 유효합니다. OmniaKey의 이미지 API 문서(2026년 11월 12일 확인)에 따르면 작업 기록과 이미지는 생성 후 24시간 보관됩니다. URL이 만료되기 전에 내려받으세요. 만료됐다면 작업 ID를 다시 폴링해 새 서명 URL을 받습니다.
애셋 URL은 일반 GET으로 내려받고 Authorization 헤더를 붙이지 않습니다. 서명은 URL 안에 있습니다. 스크립트는 내려받은 바이트를 작업의 sha256과 비교하므로 잘린 파일을 멀쩡한 복사본 위에 덮어쓰기 전에 잡아냅니다.
시간 초과나 다운로드 실패 후 다시 실행하기
같은 명령을 다시 실행하고 작업을 이어 갑니다. 스크립트는 midjourney-task.json을 읽고 가장 짧은 안전 경로를 고릅니다.
- 생성이 확인되지 않음: 같은 본문을 같은 멱등성 키로 POST합니다.
- 작업 ID가 있고 작업이 아직 실행 중: 기존 ID를 폴링합니다.
- 작업은 성공했지만 PNG가 없음: 새 서명 URL을 받기 위해 한 번 폴링하고 다시 내려받습니다.
- 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 모델 페이지에서 확인하세요. 배치를 견적 내기 전에 확인하는 것이 좋습니다.
Midjourney는 한 그리드에서 네 가지 변형을 비교할 때 잘 맞습니다. 이 연동은 현재 텍스트→이미지 그리드를 반환합니다. 호출당 이미지 한 장이 필요하거나 원본 장면을 유지하는 편집이 필요하면 GPT Image 2.5 API 또는 Nano Banana Pro API를 보세요.
코드 대신 프롬프트를 입력하고 싶다면 AI Image Generator에서 브라우저로 이미지 모델을 실행할 수 있습니다.
자주 묻는 질문
이 API를 쓰려면 Midjourney 구독이 필요한가요?
아니요. 키는 OmniaKey에서 발급하고 호출은 OmniaKey의 서드파티 API로 갑니다. OmniaKey는 Midjourney가 아닙니다. Midjourney 웹사이트 구독은 별도 제품이고 OmniaKey 키를 만들지 않습니다.
작업 하나가 이미지 네 장을 파일 하나로 반환하는 이유는 무엇인가요?
Midjourney는 후보 네 개를 2x2 그리드 PNG 한 장으로 반환합니다. 이 연동에서는 업스케일과 vary를 쓸 수 없어 네 후보가 같은 파일을 공유합니다. 파일을 따로 쓰려면 다운로드 후 그리드를 직접 나누세요.
409 idempotency_key_reused는 무슨 뜻인가요?
OmniaKey가 이미 가진 멱등성 키를 보냈지만 요청 본문이 바뀌었다는 뜻입니다. 원래 본문을 보내거나 새 키로 새 작업을 시작하세요. 같은 키로 프롬프트를 바꾸는 것이 흔한 원인입니다.
폴링 중 스크립트가 시간 초과되면 어떻게 되나요?
작업은 계속 실행됩니다. 작업 ID가 midjourney-task.json에 있으므로 같은 스크립트를 다시 실행하면 그 ID를 폴링하거나 완료된 애셋을 내려받습니다. 로컬 시간 초과는 작업과 과금을 그대로 둡니다.
이 글에서 다루는 모델
관련 글
Nano Banana Pro API: Python 호출, 이미지 편집, 가격
POST 한 번, 폴링 한 번, 다운로드 한 번. 그대로 실행되는 스크립트입니다.
GPT Image 2.5 API: Python으로 이미지 생성하고 편집하기
POST 한 번이면 작업이 만들어지고, GET 한 번이면 결과를 읽습니다. 실행한 요청 코드와 2026년 10월 29일에 시험한 크기, 편집 전후 비교를 담았습니다.
GPT Image 2.6 리뷰: GIF, SVG, 가격과 누구에게 맞나
GPT Image 2.6의 새 기능을 직접 만든 GIF, 포스터 세 장, 사진 편집으로 시험했습니다.

