Makernice API 문서: 빠른 시작
- 적합한 용도
- REST로 자체 코드에서 영상·이미지 생성을 호출하는 개발자. 견적을 받고, 생성을 요청한 뒤, 폴링하거나 웹훅으로 결과를 받습니다.
- 적합하지 않은 용도
- Makernice API는 코드를 작성하지 않고 영상이나 이미지를 만드는 용도가 아닙니다. 그런 경우에는 콘솔의 만들기 메뉴를 사용해 주십시오. 같은 모델, 같은 가격, 같은 잔액을 씁니다.
API 키 하나와 요청 세 번으로 영상을 생성합니다. 견적을 받고, 생성을 만든 뒤, 완료될 때까지 폴링합니다.
1. API 키 만들기
콘솔에서 개발자 → API 키를 열고 키를 만드십시오. 모든 요청에 Bearer 토큰으로 보냅니다. 생성 비용은 지갑에서 차감되며, 키마다 지출 한도를 걸 수 있습니다.
2. 견적 받기
curl https://api.makernice.com/v1/generations/quote \
-H "Authorization: Bearer $MAKERNICE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"seedance-2.5","mode":"t2v","prompt":"온실 안을 천천히 지나가는 돌리 숏","duration":5,"resolution":"720p"}'
# → (예시 값) { "id": "qt_…", "amount": "500000", "expires_at": "…", "facts": { "quantity": 5, … } }금액은 미국 달러 기준이며 100만분의 1달러(micro-USD) 단위입니다. 1,000,000 = $1.00입니다. 견적은 가격을 12분 동안 고정합니다.
3. 생성 만들기
curl https://api.makernice.com/v1/generations \
-H "Authorization: Bearer $MAKERNICE_API_KEY" \
-H "Idempotency-Key: 7c1e9a2b-order-1842" \
-H "Content-Type: application/json" \
-d '{"quote_id":"qt_…","input":{"model":"seedance-2.5","mode":"t2v","prompt":"온실 안을 천천히 지나가는 돌리 숏","duration":5,"resolution":"720p"}}'
# → 202 { "id": "gen_…", "status": "queued", "billing": "held", … }금액은 잔액에서 보류됩니다. 생성이 실패하거나 대기 중에 취소하면 보류가 전액 해제됩니다. 같은 Idempotency-Key로 다시 시도하면 두 번 차감되지 않고 같은 생성이 반환됩니다.
4. 완료될 때까지 폴링
curl https://api.makernice.com/v1/generations/gen_… \
-H "Authorization: Bearer $MAKERNICE_API_KEY"
# status: queued → generating → moderating → succeeded | failed | canceled
# succeeded가 되면: outputs[0].url을 키와 함께 GET 하면 파일로 302 리디렉션됩니다5. 파일 다운로드
curl -L -o video.mp4 https://api.makernice.com/v1/generations/gen_…/outputs/0 \
-H "Authorization: Bearer $MAKERNICE_API_KEY"Python: 견적, 생성, 폴링
MAKERNICE_API_KEY를 설정한 뒤 실행하십시오.
import os
import time
import uuid
from decimal import Decimal
import requests
API = "https://api.makernice.com/v1"
ORIGIN = "https://api.makernice.com"
HEADERS = {"Authorization": f"Bearer {os.environ['MAKERNICE_API_KEY']}"}
TIMEOUT_SECONDS = 30
POLL_INTERVAL_SECONDS = 5
MAX_WAIT_SECONDS = 30 * 60
FINAL = {"succeeded", "failed", "canceled"}
def call(method, path, headers=None, **kwargs):
# Waits out 429 (rate_limited, concurrency_limit_reached) using Retry-After; raises on any other error.
for _ in range(5):
r = requests.request(method, API + path, headers={**HEADERS, **(headers or {})}, timeout=TIMEOUT_SECONDS, **kwargs)
if r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", "5")))
continue
if not r.ok:
raise RuntimeError(f"{method} {path} -> {r.status_code}: {r.text}")
return r.json()
raise RuntimeError(f"{method} {path}: still rate limited")
job = {
"model": "seedance-2.5",
"mode": "t2v",
"prompt": "A slow dolly shot through a greenhouse",
"duration": 5,
"resolution": "720p",
}
# 1. Quote: the exact amount, locked for 12 minutes
quote = call("POST", "/generations/quote", json=job)
usd = Decimal(quote["amount"]).scaleb(-6) # micro-USD integer string -> dollars, no float rounding
print(f"Quoted ${usd} (price list v{quote['price_version']}), valid until {quote['expires_at']}")
# 2. Create: same input + quote_id. A retry with the same Idempotency-Key returns the same generation
idempotency_key = str(uuid.uuid4())
gen = call("POST", "/generations", json={"quote_id": quote["id"], "input": job}, headers={"Idempotency-Key": idempotency_key})
# 3. Poll: queued -> generating -> moderating -> succeeded | failed | canceled (checking: keep polling)
deadline = time.monotonic() + MAX_WAIT_SECONDS
while gen["status"] not in FINAL:
if time.monotonic() > deadline:
raise SystemExit(f"{gen['id']} is still {gen['status']}; poll it again later")
time.sleep(POLL_INTERVAL_SECONDS)
gen = call("GET", f"/generations/{gen['id']}")
if gen["status"] != "succeeded":
# billing is "released": the hold went back to your balance
raise SystemExit(f"{gen['status']}, billing {gen['billing']}: {gen['error']}")
# 4. Download: outputs[].url is relative to the API origin and redirects (302) to the file
output = gen["outputs"][0]
if output["state"] == "blocked":
raise SystemExit("Output blocked by moderation (the run is charged); contact support for a manual review")
r = requests.get(ORIGIN + output["url"], headers=HEADERS, timeout=120) # requests drops Authorization on the redirect
r.raise_for_status()
with open("output.mp4", "wb") as f:
f.write(r.content)
print(f"Saved output.mp4, charged ${Decimal(gen['amount']).scaleb(-6)}")Node.js: 견적, 생성, 폴링
Node.js 18 이상이 필요하며 별도 패키지는 필요 없습니다. quickstart.mjs로 저장하고 MAKERNICE_API_KEY를 설정한 뒤 node quickstart.mjs를 실행하십시오.
// Node.js 18+ (built-in fetch, no packages). Save as quickstart.mjs and run: node quickstart.mjs
import { randomUUID } from "node:crypto";
import { writeFile } from "node:fs/promises";
const API = "https://api.makernice.com/v1";
const ORIGIN = "https://api.makernice.com";
const HEADERS = { Authorization: `Bearer ${process.env.MAKERNICE_API_KEY}` };
const TIMEOUT_MS = 30_000;
const POLL_INTERVAL_MS = 5_000;
const MAX_WAIT_MS = 30 * 60 * 1000;
const FINAL = new Set(["succeeded", "failed", "canceled"]);
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// micro-USD integer string -> dollars, no float rounding
const usd = (micro) => `${BigInt(micro) / 1_000_000n}.${(BigInt(micro) % 1_000_000n).toString().padStart(6, "0")}`;
class Stop extends Error {}
async function call(method, path, { body, headers } = {}) {
// Waits out 429 (rate_limited, concurrency_limit_reached) using Retry-After; throws on any other error.
for (let attempt = 0; attempt < 5; attempt++) {
const r = await fetch(API + path, { signal: AbortSignal.timeout(TIMEOUT_MS),
method,
headers: { ...HEADERS, ...(body ? { "Content-Type": "application/json" } : {}), ...headers },
body: body ? JSON.stringify(body) : undefined,
});
if (r.status === 429) {
await sleep(Number(r.headers.get("Retry-After") ?? "5") * 1000);
continue;
}
if (!r.ok) throw new Stop(`${method} ${path} -> ${r.status}: ${await r.text()}`);
return r.json();
}
throw new Stop(`${method} ${path}: still rate limited`);
}
async function main() {
const job = {
"model": "seedance-2.5",
"mode": "t2v",
"prompt": "A slow dolly shot through a greenhouse",
"duration": 5,
"resolution": "720p",
};
// 1. Quote: the exact amount, locked for 12 minutes
const quote = await call("POST", "/generations/quote", { body: job });
console.log(`Quoted $${usd(quote.amount)} (price list v${quote.price_version}), valid until ${quote.expires_at}`);
// 2. Create: same input + quote_id. A retry with the same Idempotency-Key returns the same generation
const idempotencyKey = randomUUID();
let gen = await call("POST", "/generations", { body: { quote_id: quote.id, input: job }, headers: { "Idempotency-Key": idempotencyKey } });
// 3. Poll: queued -> generating -> moderating -> succeeded | failed | canceled (checking: keep polling)
const deadline = Date.now() + MAX_WAIT_MS;
while (!FINAL.has(gen.status)) {
if (Date.now() > deadline) throw new Stop(`${gen.id} is still ${gen.status}; poll it again later`);
await sleep(POLL_INTERVAL_MS);
gen = await call("GET", `/generations/${gen.id}`);
}
if (gen.status !== "succeeded") {
// billing is "released": the hold went back to your balance
throw new Stop(`${gen.status}, billing ${gen.billing}: ${JSON.stringify(gen.error)}`);
}
// 4. Download: outputs[].url is relative to the API origin and answers 302 with a signed file URL.
// Follow it yourself so your API key is never sent to the file host.
const [output] = gen.outputs;
if (output.state === "blocked") throw new Stop("Output blocked by moderation (the run is charged); contact support for a manual review");
const hop = await fetch(ORIGIN + output.url, { headers: HEADERS, redirect: "manual", signal: AbortSignal.timeout(TIMEOUT_MS) });
const location = hop.headers.get("Location");
if (hop.status < 300 || hop.status > 399 || !location) throw new Stop(`GET ${output.url} -> ${hop.status}`);
const file = await fetch(new URL(location, ORIGIN + output.url), { signal: AbortSignal.timeout(120_000) });
if (!file.ok) throw new Stop(`download -> ${file.status}`);
await writeFile("output.mp4", Buffer.from(await file.arrayBuffer()));
console.log(`Saved output.mp4, charged $${usd(gen.amount)}`);
}
main().catch((err) => {
console.error(err instanceof Stop ? err.message : err);
process.exit(1);
});여러 생성을 동시에 실행하려면 병렬로 시작하십시오(예: 스레드 풀, 비동기 HTTP 클라이언트를 쓰는 asyncio, 또는 Promise.all). 계정마다 기본적으로 동시에 진행 중인 생성은 2개입니다. 이를 넘으면 API가 Retry-After와 함께 429 concurrency_limit_reached를 반환하며, 두 예제의 call()은 이 시간만큼 기다린 뒤 다시 시도합니다. 한도를 올리려면 고객 지원에 문의하십시오.
인증
GET /v1/models와 GET /v1/pricing을 제외한 모든 요청은 Authorization 헤더에 API 키가 필요합니다. 키는 mn- 뒤에 영문자와 숫자 48자가 붙은 형태입니다.
curl https://api.makernice.com/v1/generations \
-H "Authorization: Bearer $MAKERNICE_API_KEY"- 전체 키는 만들 때 한 번만 표시됩니다. Makernice는 지문(fingerprint)만 저장하므로 잃어버린 키는 복구할 수 없습니다. 새 키를 만들고 기존 키를 삭제하십시오.
- 키가 없거나 형식이 잘못되었거나 알 수 없거나 비활성화·만료·삭제된 경우에는 항상 같은
401 invalid_api_key응답을 받습니다. 키를 비활성화하거나 삭제하면 다음 요청부터 적용됩니다. - API는 서버에서 호출하십시오. 교차 출처 브라우저 요청을 받지 않으므로(CORS 미지원) 프런트엔드 코드에 키를 넣지 마십시오.
- 한 계정의 모든 키는 같은 생성을 봅니다. 어떤 키로든 콘솔에서 만든 것을 포함한 계정의 모든 생성을 조회, 목록 확인, 취소할 수 있습니다.
- 콘솔에서 키마다 만료일과 지출 한도를 설정할 수 있습니다. 키의 지출액은 그 키로 만든 생성의 차감 금액과 현재 보류 중인 금액의 합계이며, 한도를 넘게 되는 생성 요청은
402 key_spend_limit_reached를 받습니다. 실패하거나 취소된 생성은 포함되지 않습니다. - 한 계정의 모든 키는 같은 선불 잔액에서 차감됩니다. 현재 Makernice 계정 하나에는 로그인이 하나만 있으며, 팀은 여러 API 키로 하나의 잔액을 함께 사용합니다.
- API 키 페이지에서 키별 누적 지출을, 사용량 페이지에서 키와 모델별 API 지출을 확인할 수 있습니다.
요금 및 결제
- 요금은 미국 달러로 청구됩니다. 모든 금액은 micro-USD(100만분의 1달러) 단위의 정수 문자열입니다.
1,000,000= $1.00입니다. 부동소수점 숫자가 아니라 정수나 십진수(decimal)로 파싱하십시오. - 영상은 초당 가격이 매겨지고(수량은 요청한 길이이며, 참조 영상 클립이 있는 템플릿은 클립의 초도 포함), 이미지는 요청당 가격이 매겨집니다(생성 1건에 이미지 1장). 가격 = 단가 × 수량입니다.
- 견적은 가격을 12분 동안 고정합니다(
expires_at). 그사이 가격이 바뀌더라도 생성에는 견적 금액이 차감됩니다.
GET /v1/pricing은 현재 가격표 버전의 단가를 나열합니다. API 키가 필요 없으며 최대 60초 동안 캐시될 수 있습니다. official_unit_price가 있으면 같은 조합의 공식 정가로, 비교용일 뿐입니다. 실제로는 항상 unit_price가 청구됩니다.
{
"price_version": 3,
"data": [
{ "model": "seedance-2.5", "mode": "t2v", "resolution": "720p", "unit": "second", "unit_price": "100000", "unit_price_usd": "0.10", "official_unit_price": "120000" }
]
}실패한 생성도 차감됩니까?
아니요. 보류가 전액 해제되며, 실패한 생성은 차감되지 않습니다. 결과물이 정책 위반으로 검수에서 차단된 경우에는 차감됩니다. 고객 지원에 수동 검토를 요청할 수 있습니다.
생성을 만들면 견적 금액이 보류됩니다. 보류 금액은 생성이 완료될 때 차감으로 전환되며, 실패하거나 타임아웃되면 전액 해제되고, 대기열에서 취소한 경우에도 전액 해제됩니다. 해제는 환불이 아닙니다. 그 돈은 잔액에서 빠져나간 적이 없습니다.
| 상황 | billing | 차감 여부 |
|---|---|---|
| 생성이 성공함 | charged | 차감됨, 견적 금액 그대로 |
생성이 실패함(error.code가 generation_failed, upstream_unavailable 또는 invalid_request) | released | 차감되지 않음 |
최대 실행 시간을 넘김(error.code: timeout) | released | 차감되지 않음 |
queued 상태에서 취소함 | released | 차감되지 않음 |
생성을 만들 때 프롬프트가 차단됨(422 content_rejected, reason: prompt) | — | 차감되지 않음. 생성이 만들어지지 않고 보류도 없음 |
입력 이미지 또는 모델 제공사의 안전 정책이 거부함(content_rejected, reason이 input_image 또는 model) | released | 차감되지 않음 |
완성된 결과물이 결과물 검수에서 차단됨(결과물 state: blocked) | charged | 차감됨. 고객 지원에 수동 검토를 요청할 수 있음 |
결과를 확인하는 중(status: checking) | held | 성공으로 확정된 경우에만 차감됨 |
견적 받기
생성 입력을 보내면 응답에서 그 생성에 드는 정확한 비용을 알려 줍니다. 견적 하나는 생성 1건에 사용할 수 있습니다.
| 필드 | 타입 | 설명 |
|---|---|---|
model | string | 필수. GET /v1/models의 모델 id. |
mode | string | 필수. t2v, i2v, flf, r2v, t2i 또는 i2i. 모델마다 지원하는 모드가 나열되어 있습니다. |
prompt | string | 필수. 기본 1–4,000자이며, 일부 모델은 더 짧습니다(GET /v1/models의 maxPromptChars 참조). |
duration | integer | 영상 전용. 모델 범위 내의 초 단위 값이며, 생략하면 모델 기본값을 씁니다. |
resolution | string | 모델의 resolutions 중 하나. 생략하면 defaultResolution을 씁니다. 모델에 나열된 값이 없으면 보낼 수 없습니다. |
aspect_ratio | string | 모드의 aspectRatios 중 하나. |
image_url | string | 첫 프레임. i2v와 flf에서 필수. |
last_image_url | string | 마지막 프레임. flf에서 필수. |
reference_images | string[] | r2v와 i2i에서 필수. 최대 모드의 maxReferences개까지. |
seed | integer | 선택. 0–2,147,483,647. 같은 설정에 같은 seed를 쓰면 더 비슷한 결과가 나옵니다. |
generate_audio | boolean | 선택. 영상에 사운드트랙을 넣을지 여부. |
prompt_expansion | string | 선택. off, balanced 또는 quality. 모델이 프롬프트를 얼마나 고쳐 쓸 수 있는지 정합니다. |
negative_prompt | string | 선택. 피하고 싶은 요소를 최대 2,000자로 설명합니다. |
watermark | boolean | 선택. 제공업체의 AI 생성 워터마크를 추가합니다. |
마지막 다섯 개 필드는 선택 옵션입니다. 모델마다 그중 일부만 지원합니다. GET /v1/models의 prices[].options에 모드와 해상도별로 사용 가능한 옵션이 허용 값, 기본값과 함께 나열됩니다. 어떤 모델 경로에도 적용할 수 없는 필드는 400 invalid_request를 반환하며, 선택 필드를 바꿀 때도 다른 필드와 마찬가지로 새 견적이 필요합니다.
이미지는 공개 https URL(최대 2,048자)로 보냅니다. 모드에서 쓰지 않는 필드와 알 수 없는 필드는 400 invalid_request로 거부됩니다. 입력 필드의 경우 error.details에 각 문제가 { path, message } 형태로 나열됩니다(그 밖의 invalid_request 오류는 details 형태가 다르거나 없을 수 있습니다). 가격이 없는 조합은 422 price_unavailable를 반환합니다.
각 이미지는 공개적으로 접근 가능한 https URL이어야 하며, 그 URL에서 직접 정지 JPEG, PNG 또는 WebP 이미지(애니메이션 WebP와 GIF는 지원하지 않음)를 최대 10 MB로 제공해야 합니다. 리디렉션은 따라가지 않습니다. Makernice는 각 이미지를 한 번 가져와 사본을 보관하며, 콘텐츠 검수와 모델 모두 그 사본을 사용합니다. 이미지를 사용할 수 없으면 생성이 invalid_request로 실패하고, error.input에 해당 이미지가 표시되며, 보류는 해제됩니다.
curl https://api.makernice.com/v1/generations/quote \
-H "Authorization: Bearer $MAKERNICE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"seedance-2.5","mode":"t2v","prompt":"온실 안을 천천히 지나가는 돌리 숏","duration":5,"resolution":"720p"}'{
"id": "qt_6f1c…",
"amount": "500000",
"price_version": 3,
"expires_at": "2026-10-09T09:24:41.000Z",
"facts": { "model": "seedance-2.5", "mode": "t2v", "resolution": "720p", "variant": "default", "unit": "second", "quantity": 5 }
}견적은 가격을 얼마나 고정합니까?
모든 견적은 12분간 가격이 고정되며, 생성에는 항상 견적 금액 그대로 차감됩니다.
| 필드 또는 경우 | 의미 |
|---|---|
expires_at | 견적을 만든 시각으로부터 12분 뒤입니다. 그 전에 생성을 만드십시오. |
amount | 단가 × facts.quantity(영상은 초, 이미지는 1)이며 단위는 micro-USD입니다. 생성을 만들 때 보류되고, 생성이 성공하면 차감됩니다. |
price_version | 견적을 받은 가격표 버전입니다. 생성을 만들기 전에 새 가격표가 게시되어도 견적 금액이 차감됩니다. |
| 12분이 지난 경우 | 이 견적으로 생성을 만들면 410 quote_expired를 반환합니다. 보류된 금액은 없으며, 새 견적을 받으십시오. |
| 모델, 모드, 해상도 또는 길이를 바꾼 경우 | 422 quote_mismatch를 반환합니다. 프롬프트만 바꾸면 견적은 그대로 유효합니다. |
| 이미 사용한 견적 | 409 quote_already_used를 반환합니다. 견적 하나로 생성 하나만 만들 수 있습니다. |
생성 요청
견적과 같은 입력으로 생성을 만듭니다. Idempotency-Key 헤더는 필수입니다(멱등성 참조).
curl https://api.makernice.com/v1/generations \
-H "Authorization: Bearer $MAKERNICE_API_KEY" \
-H "Idempotency-Key: 7c1e9a2b-order-1842" \
-H "Content-Type: application/json" \
-d '{"quote_id":"qt_6f1c…","input":{"model":"seedance-2.5","mode":"t2v","prompt":"온실 안을 천천히 지나가는 돌리 숏","duration":5,"resolution":"720p"}}'- 생성(
status: queued,billing: held)과 함께202로 응답합니다. 같은 Idempotency-Key로 다시 시도하면 원래 생성과 함께200으로 응답합니다. - 프롬프트는 바꿀 수 있지만, 가격에 영향을 주는 항목(모델, 모드, 해상도, 길이)을 바꾸면
422 quote_mismatch를 반환합니다. - 이미 사용한 견적은
409 quote_already_used, 만료된 견적은410 quote_expired, 계정에 없는 견적은404 quote_not_found를 반환합니다. - 잔액이 부족하면
402 insufficient_balance, 키의 지출 한도를 넘으면402 key_spend_limit_reached를 반환합니다. - 최상위
callback_url은 선택 항목입니다(https, 최대 2048자, URL에 자격 증명 포함 불가, 사설 또는 예약 주소 불가). 생성이 끝나면 그 주소로 생성을 POST 합니다. 계정의 웹훅 주소보다 우선합니다. 웹훅을 참조하십시오.
상태 확인
생성이 최종 상태에 도달할 때까지 폴링하십시오. 몇 초 간격으로 폴링해도 됩니다. 요청 한도의 읽기 한도를 참조하십시오. 영상 생성은 몇 분이 걸릴 수 있으며, 긴 영상이나 높은 해상도는 더 오래 걸립니다. 최대 실행 시간을 넘기면 생성이 실패하고 보류가 전액 해제됩니다.
| 상태 | 의미 |
|---|---|
queued | 시작 대기 중입니다. 아직 취소할 수 있습니다. |
generating | 모델이 생성 중입니다. |
moderating | 생성이 끝나 금액이 차감되었습니다. 결과물이 안전 검사 중이며, 검사가 끝날 때까지 outputs는 비어 있습니다. 계속 폴링하십시오. |
succeeded | 완료. 금액이 차감되었고 outputs가 준비되었습니다. |
failed | 완료되지 못했습니다. 보류가 해제되며, error에 코드와 메시지가 들어 있습니다. |
canceled | 대기 중에 취소되었습니다. 보류가 해제됩니다. |
checking | 결과를 확인하는 중입니다. 확정될 때까지 금액은 보류된 상태로 유지됩니다. |
실패한 생성의 error.code는 invalid_request(모델이 요청을 거부함. 프롬프트나 설정을 조정하십시오), content_rejected(콘텐츠 검수 또는 모델 제공업체의 안전 정책에 의해 거부됨. 보류는 해제됩니다), timeout, upstream_unavailable, generation_failed 중 하나입니다. content_rejected의 경우 error.reason에 거부된 대상이 표시됩니다. prompt(프롬프트), input_image(참조 이미지), model(모델 제공업체의 안전 정책) 중 하나입니다. 제출한 프롬프트가 차단될 때의 422 응답에도 같은 reason이 들어 있습니다. 사용할 수 없는 참조 이미지 때문에 invalid_request가 발생하면 error.input에 해당 이미지가 표시됩니다. image_url, last_image_url 또는 reference_images[0](0부터 시작) 형식입니다. 그 이미지를 교체하고 새 생성을 만드십시오.
{
"id": "gen_3b9e…",
"type": "video",
"model": "seedance-2.5",
"mode": "t2v",
"status": "succeeded",
"prompt": "온실 안을 천천히 지나가는 돌리 숏",
"params": { "duration": 5, "resolution": "720p", "aspectRatio": null },
"amount": "500000",
"billing": "charged",
"price_version": 3,
"outputs": [
{
"index": 0,
"url": "/v1/generations/gen_3b9e…/outputs/0",
"state": "stored",
"contentType": "video/mp4",
"expiresAt": "2026-11-08T09:14:03.000Z"
}
],
"error": null,
"created_at": "2026-10-09T09:12:41.000Z",
"finished_at": "2026-10-09T09:14:03.000Z"
}생성이 끝나고 90일이 지나면 데이터 보존 정책에 따라 요청 내용이 삭제되어 prompt가 빈 문자열이 됩니다. 청구 정보(model, params, amount, status, 타임스탬프)는 유지됩니다.
GET /v1/generations/{id}/events는 생성의 타임라인을 반환합니다. 코드: created, submitted, generating, delivered, failed_refunded, canceled_refunded, checking, requeued.
{
"data": [
{ "code": "created", "at": "2026-10-09T09:12:41.000Z" },
{ "code": "submitted", "at": "2026-10-09T09:12:43.000Z" },
{ "code": "generating", "at": "2026-10-09T09:12:50.000Z" },
{ "code": "delivered", "at": "2026-10-09T09:14:03.000Z" }
]
}결과물 다운로드
outputs의 각 항목에는 /v1/generations/gen_…/outputs/0처럼 https://api.makernice.com 기준 상대 경로인 url이 있습니다. API 키와 함께 요청하면 파일로 가는 단기 링크로의 302 리디렉션이 응답됩니다. 리디렉션 대상을 저장하지 말고, 파일이 필요할 때마다 엔드포인트를 다시 요청하십시오.
curl -L -o video.mp4 https://api.makernice.com/v1/generations/gen_…/outputs/0 \
-H "Authorization: Bearer $MAKERNICE_API_KEY"?download=1을 붙이면 파일이 첨부 파일로 제공됩니다. 파일이stored상태일 때 작동하며,pending또는degraded상태에서는 원본 파일로 리디렉션되고 이 매개변수는 효과가 없습니다.curl -L은 리디렉션 대상 호스트에Authorization헤더를 보내지 않으므로 키는 Makernice 밖으로 나가지 않습니다.- 결과물은 생성이
succeeded일 때만 존재합니다. 그 밖의 경우 엔드포인트는404 output_not_found를 반환합니다. state:pending(아직 저장 중이며 링크는 이미 작동함),stored(expiresAt까지 저장됨),degraded(저장이 완료되지 않아 링크가 더 일찍 작동을 멈출 수 있음 — 서둘러 다운로드하십시오),expired(저장된 파일이 만료됨. 엔드포인트가410 output_expired를 반환함),blocked(결과물이 콘텐츠 검수를 통과하지 못함.url은 빈 문자열 — 수동 검토는 고객 지원에 문의하십시오).
취소
queued 상태의 생성만 취소할 수 있으며, 보류가 전액 해제되고 취소된 생성이 응답으로 반환됩니다. 생성이 generating상태가 되면 중지할 수 없고, 취소 요청은 409 not_cancelable을 반환합니다.
curl -X POST https://api.makernice.com/v1/generations/gen_…/cancel \
-H "Authorization: Bearer $MAKERNICE_API_KEY"
# → { "id": "gen_…", "status": "canceled", "billing": "released", … }생성 목록
콘솔에서 만든 것을 포함해 계정의 생성을 최신순으로 나열합니다.
| 쿼리 | 설명 |
|---|---|
limit | 페이지 크기, 1–100. 기본값 20. |
cursor | 이전 페이지의 next_cursor. |
status | 하나 이상의 상태를 쉼표로 구분. 예: queued,generating. |
type | video 또는 image. |
model | 모델 id. |
q | 생성 id의 일부 또는 프롬프트에 포함된 텍스트(둘 다 대소문자 구분 없음). |
curl "https://api.makernice.com/v1/generations?status=queued,generating&limit=50" \
-H "Authorization: Bearer $MAKERNICE_API_KEY"
# → { "data": [ …생성 목록… ], "next_cursor": "gen_…" | null, "total": 12 }total은 필터에 맞는 모든 생성의 수입니다. next_cursor가 null이면 마지막 페이지입니다.
웹훅
폴링 대신, 생성이 최종 상태에 도달하면 Makernice가 생성을 서버로 POST 하도록 할 수 있습니다. 생성을 만들 때 지정한 callback_url로 보내며, 지정하지 않았으면 계정의 웹훅 주소(콘솔 설정에서 설정)로 보냅니다. 둘 다 없으면 아무것도 보내지 않습니다.
| 이벤트 | 전송 시점 |
|---|---|
generation.succeeded | 생성이 성공하여 차감되었을 때. |
generation.failed | 생성이 실패하여 보류 금액이 해제되었을 때. |
generation.canceled | 생성이 취소되어 보류 금액이 해제되었을 때. |
요청
POST /your/webhook/path HTTP/1.1
Content-Type: application/json
Makernice-Signature: t=1791364443,v1=5f0c2d…e81a
Makernice-Event-Id: evt_1b7f…{
"id": "evt_1b7f…",
"type": "generation.succeeded",
"created_at": "2026-10-09T09:14:03.000Z",
"data": {
"id": "gen_3b9e…",
"status": "succeeded",
"billing": "charged",
"outputs": [
{ "index": 0, "url": "https://api.makernice.com/v1/generations/gen_3b9e…/outputs/0", "state": "stored", … }
],
…
}
}data는GET /v1/generations/{id}와 같은 생성 객체이며,outputs[].url만 절대 URL입니다.Makernice-Event-Id는 본문의id와 같고 재시도 간에도 바뀌지 않습니다. 중복을 무시하는 데 사용하십시오.
서명 검증
Makernice-Signature는 t=<unix seconds>,v1=<hex> 형식이며, v1은 서명 시크릿으로 t + "." + raw body에 대해 계산한 HMAC-SHA256입니다. 받은 원본 바이트 그대로 계산하고, 상수 시간으로 비교하며, t가 서버 시각과 5분 넘게 차이 나는 요청은 거부하십시오. 서명 시크릿은 콘솔 설정에 있으며, callback_url 전송에도 같은 시크릿으로 서명합니다. 시크릿을 교체하면 아직 보내지 않은 전송은 새 시크릿으로 서명됩니다.
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody: the body exactly as received (Buffer). Don't re-serialize parsed JSON.
export function verifyWebhook(rawBody, signatureHeader, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(signatureHeader.split(",").map((p) => [p.slice(0, p.indexOf("=")), p.slice(p.indexOf("=") + 1)]));
const t = Number(parts.t);
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = createHmac("sha256", secret).update(`${t}.`).update(rawBody).digest();
const given = Buffer.from(parts.v1 ?? "", "hex");
return given.length === expected.length && timingSafeEqual(given, expected);
}import hashlib
import hmac
import time
# raw_body: the body exactly as received (bytes). Don't re-serialize parsed JSON.
def verify_webhook(raw_body: bytes, signature_header: str, secret: str, tolerance_seconds: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in signature_header.split(",") if "=" in p)
try:
t = int(parts["t"])
except (KeyError, ValueError):
return False
if abs(time.time() - t) > tolerance_seconds:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))전송과 재시도
- HTTPS만 지원합니다. 리디렉션은 따라가지 않습니다. 각 시도는 10초 후 시간 초과됩니다.
- 2xx 응답이면 전송된 것으로 봅니다. 빠르게 응답하고 무거운 작업은 그 뒤에 처리하십시오.
- 그렇지 않으면 1 min, 5 min, 30 min, 2 h, 6 h, 12 h 후에 재시도하며(총 7회 시도), 그래도 실패하면 전송 실패로 표시합니다. 최근 전송과 마지막 응답은 콘솔 설정에 표시됩니다.
- 생성마다 보통 이벤트 하나를 보내며, 같은 전송이 두 번 이상 도착할 수 있습니다(예: 수신 측에서 시간 초과가 난 뒤). 콘텐츠 검토로 보류되었던 결과물이 나중에 공개되면 새 이벤트 id로 생성을 다시 보내므로 이벤트를 두 개 받을 수 있으며, 가장 최근 것이 유효합니다.
모델
GET /v1/models는 사용할 수 있는 모델과 모드, 해상도, 길이를 나열합니다. 가격이 있는 모드만 나열되며, 가격은 modes[].prices에 있습니다. API 키가 필요 없으며 최대 60초 동안 캐시될 수 있습니다.
resolutions/defaultResolution:resolution에 허용되는 값. 목록이 비어 있으면 해상도가 고정된 모델이므로resolution을 생략합니다(고정값이라도 보내면400 invalid_request를 반환). 고정 등급은modes[].prices[].resolution에 표시된 값입니다. 예를 들어seedance-2.0-fast는 항상 720p로 렌더링합니다. 나열된 해상도라도 일부 모드에서는 가격이 없을 수 있으니modes[].prices를 확인하십시오. 그런 해상도로 견적을 요청하면422 price_unavailable를 반환합니다.durations: 영상은 초 단위{ min, max, default }, 이미지는null.modes[].needs: 모드에 필요한 이미지(image→image_url,lastImage→last_image_url,references→reference_images, 최대maxReferences개).modes[].aspectRatios: 허용되는aspect_ratio값.modes[].prices: 해상도별 단가.
{
"data": [
{
"id": "seedance-2.5",
"type": "video",
"resolutions": ["480p", "720p", "1080p", "4k"],
"defaultResolution": "720p",
"durations": { "min": 4, "max": 30, "default": 5 },
"modes": [
{
"mode": "t2v",
"needs": { "image": false, "lastImage": false, "references": false },
"maxReferences": null,
"aspectRatios": ["adaptive", "16:9", "9:16", "1:1", "4:3", "3:4", "21:9", "9:21"],
"prices": [{ "resolution": "720p", "unit": "second", "unitPrice": "100000" }]
}
]
}
]
}어떤 모델을 호출할 수 있습니까?
API 키 하나로 ByteDance, Kuaishou, MiniMax, Alibaba의 영상 모델 13개와 이미지 모델 4개, 총 17개 모델을 모두 호출할 수 있으며, Seedance, Kling, Wan, MiniMax H3가 포함됩니다. 모든 모델이 같은 요청 형식을 쓰므로 model을 아래의 API 모델 id로 지정하면 됩니다.
| 모델 | 제공사 | API 모델 id | 유형 |
|---|---|---|---|
| Seedance 2.5 | ByteDance | seedance-2.5 | 영상 |
| Seedance 2.0 | ByteDance | seedance-2.0 | 영상 |
| Seedance 2.0 Fast | ByteDance | seedance-2.0-fast | 영상 |
| MiniMax H3 | MiniMax | minimax-h3 | 영상 |
| MiniMax H3 Max | MiniMax | minimax-h3-max | 영상 |
| MiniMax H3 Max Turbo | MiniMax | minimax-h3-max-turbo | 영상 |
| Wan 3.0 | Alibaba | wan-3.0 | 영상 |
| Wan 3.0 Prime | Alibaba | wan-3.0-prime | 영상 |
| Kling 3.0 Pro | Kuaishou | kling-3.0-pro | 영상 |
| Kling 3.0 Standard | Kuaishou | kling-3.0-standard | 영상 |
| Kling 3.0 4K | Kuaishou | kling-3.0-4k | 영상 |
| Kling 4.0 | Kuaishou | kling-4.0 | 영상 |
| Happy Horse 1.1 | Alibaba | happy-horse-1.1 | 영상 |
| Seedream 5.0 Pro | ByteDance | seedream-5.0-pro | 이미지 |
| Seedream 5.0 | ByteDance | seedream-5.0 | 이미지 |
| Qwen3 Image | Alibaba | qwen3-image | 이미지 |
| Qwen3 Pro Image | Alibaba | qwen3-pro-image | 이미지 |
작업별 모델, 지원 모드, 예시 가격(영어 페이지): image to video, AI video generator, AI image generator.
오류
오류는 JSON입니다: { "error": { "code", "message", "reason"?, "details"?, "request_id" } }. 분기는 code로 하십시오. 메시지는 바뀔 수 있습니다. 모든 응답에는 x-request-id 헤더(request_id와 같은 값)가 포함되며, 고객 지원에 문의할 때 함께 알려 주시면 해당 요청을 찾을 수 있습니다.
| 상태 | 코드 | 조치 |
|---|---|---|
| 400 | invalid_request | 본문, 쿼리 또는 헤더가 유효하지 않습니다. details에 각 필드와 문제가 나열되어 있으니 요청을 수정하십시오. |
| 400 | template_conflict | 템플릿 요청에는 template, images와 선택 항목인 prompt, resolution, aspect_ratio만 넣을 수 있습니다. 모델, 모드, 길이 등 나머지 설정은 템플릿이 정합니다. 다른 필드를 제거하십시오. |
| 401 | invalid_api_key | 키가 없거나 형식이 잘못되었거나 비활성화·만료·삭제되었습니다. |
| 402 | insufficient_balance | 지갑을 충전하십시오. |
| 402 | key_spend_limit_reached | 키의 지출 한도를 올리거나 다른 키를 사용하십시오. |
| 402 | daily_spend_limit_reached | 계정이 일일 지출 한도에 도달했습니다. 한도는 매일 00:00 UTC에 초기화됩니다. 한도를 올리려면 고객 지원에 문의하십시오. |
| 403 | org_frozen | 계정이 동결되었습니다. 고객 지원에 문의하십시오. |
| 404 | quote_not_found | 계정에 해당 견적이 없습니다. 새 견적을 받으십시오. |
| 404 | task_not_found | 계정에 해당 생성이 없습니다. id를 확인하십시오. |
| 404 | output_not_found | 생성이 아직 성공하지 않았거나 해당 index의 결과물이 없습니다. |
| 404 | template_not_found | 이 id의 템플릿이 없습니다. GET /v1/templates로 사용 가능한 템플릿 목록을 확인하십시오. |
| 404 | not_found | 알 수 없는 엔드포인트입니다. 메서드와 경로를 확인하십시오. |
| 409 | quote_already_used | 새 견적을 받으십시오. |
| 409 | not_cancelable | 대기 중(queued)인 생성만 취소할 수 있습니다. |
| 410 | quote_expired | 새 견적을 받으십시오. |
| 410 | output_expired | 저장된 파일이 만료되어 더 이상 다운로드할 수 없습니다. |
| 410 | template_retired | 이 템플릿은 더 이상 제공되지 않습니다. GET /v1/templates에서 다른 템플릿을 고르십시오. 이미 만든 생성에는 영향이 없습니다. |
| 413 | payload_too_large | 요청 본문이 64 KB를 넘습니다. 프롬프트를 줄이거나 이미지 URL 수를 줄이십시오. |
| 422 | idempotency_conflict | 이 Idempotency-Key는 다른 요청에 이미 사용되었습니다. 새 키를 사용하십시오. |
| 422 | quote_mismatch | 입력이 가격에 영향을 주는 항목(모델, 모드, 해상도, 길이)을 바꿨습니다. 이 입력으로 다시 견적을 받으십시오. |
| 422 | price_unavailable | 이 조합에는 가격이 없습니다. GET /v1/models에 나열된 조합을 고르십시오. |
| 422 | content_rejected | 콘텐츠 검수 또는 모델 제공업체의 안전 정책에 의해 거부되었습니다. error.reason에 거부된 대상(prompt, input_image 또는 model)이 표시됩니다. 프롬프트나 이미지를 바꾸십시오. 이번 요청은 차감되지 않았습니다. |
| 429 | rate_limited | Retry-After에 표시된 초만큼 기다린 뒤 다시 시도하십시오. |
| 429 | concurrency_limit_reached | 계정에서 진행 중인 생성이 너무 많습니다. 하나가 끝날 때까지 기다리거나(Retry-After 참조) 고객 지원에 한도 상향을 문의하십시오. |
| 500 | internal_error | 예기치 않은 서버 오류입니다. 백오프를 두고 다시 시도하십시오. 고객 지원에 문의할 때는 request_id를 함께 알려 주십시오. |
예기치 않은 5xx 응답이나 네트워크 오류는 백오프를 두고 다시 시도하십시오. 생성 요청을 다시 시도할 때는 같은 Idempotency-Key를 재사용하면 두 번 차감되지 않습니다.
요청 한도
한도는 분당 기준입니다. 초과하면 Retry-After 헤더(초)와 함께 429 rate_limited를 반환합니다.
| 범위 | 한도 |
|---|---|
| 키별 — 쓰기(견적, 생성, 취소) | 60 / 분 |
| 키별 — 읽기(조회, 목록, 이벤트, 결과물) | 600 / 분 |
| 계정별 — 모든 키의 쓰기 합계 | 120 / 분 |
| IP별 — models, pricing 및 거부된 키로 보낸 요청 | 120 / 분 |
또한 키 하나당 동시에 진행 중인 생성 요청은 최대 4개이며, 그 이상도 429를 반환합니다. 한도는 조정될 수 있습니다.
계정마다 사용 한도도 두 가지 있으며, 기본값은 동시에 진행 중인 생성 2개와 UTC 기준 하루 US$100 지출입니다. 초과하면 429 concurrency_limit_reached(Retry-After 포함) 또는 402 daily_spend_limit_reached를 반환합니다. 한도를 올리려면 고객 지원에 문의하십시오. 생성은 queued, generating 또는 checking 상태인 동안 어떤 모델이나 키로 만들었는지와 관계없이 동시 진행 한도에 포함됩니다. moderating, succeeded, failed 또는 canceled에 도달하면 더 이상 포함되지 않습니다. 웹 앱에서 시작한 생성도 같은 한도에 포함됩니다.
멱등성
POST /v1/generations에는Idempotency-Key헤더가 필요합니다. 공백 없는 출력 가능한 ASCII 문자 1–200자입니다. 새 생성마다 UUID 같은 새 값을 사용하십시오.- 키는 계정 안에서 고유하며, 계정의 모든 API 키가 공유합니다.
- 같은 키, 같은 견적, 같은 입력으로 요청을 반복하면 원래 생성이
200으로 반환되며 다시 차감되지 않습니다. - 같은 키를 다른
quote_id, 다른input(프롬프트와 이미지 URL 포함) 또는 다른callback_url과 함께 사용하면422 idempotency_conflict를 반환합니다. - 견적과 취소에는 Idempotency-Key가 필요 없습니다. 이미 성공한 취소를 반복하면
409 not_cancelable을 반환하니,GET으로 상태를 확인하십시오.
자주 묻는 질문
견적에도 비용이 듭니까?
POST /v1/generations/quote는 입력값의 가격만 계산합니다. 그 견적으로 생성을 요청하기 전까지는 보류도 차감도 일어나지 않습니다.모델마다 API 키를 따로 만들어야 합니까?
model 필드를 변경하면 됩니다.