개발자
Monstra API v1
웹 화면에서 하는 일 — 상품 등록, 사진 넣기, 상품 소개 생성·번역, 결과 받기 — 을 내 프로그램에서 그대로 할 수 있습니다. 이 문서는 그 프로그램을 만드는 개발자를 위한 것입니다.
1.시작하기
모든 요청은 아래 기본 주소로 보내고, Authorization: Bearer mk_live_… 헤더에 API 키를 넣습니다.
https://<api-host>/api/v1- 요청·응답 본문은 JSON 이고 필드 이름은
snake_case입니다. id 는 정수, 시각은 UTC 의 ISO 8601 문자열(2026-10-09T05:00:00.000Z)입니다. - 실패하면
{"error": "…", "code": "…"}를 돌려줍니다.error는 사람에게 보여 줄 한국어 문장이고 바뀔 수 있으니, 프로그램은code로 분기하세요. 일부 오류는product_id·import_id·url같은 필드를 같은 단계에 덧붙입니다. - 응답에 모르는 필드가 있으면 무시하세요(버전 정책). 요청에 모르는 필드를 넣으면 400
validation_failed입니다. - API 키는 서버에서만 씁니다.
Origin헤더가 붙은 요청(브라우저)은 403browser_not_allowed로 거절됩니다. - API 키는
/api/v1경로와 응답 안의 서명된 파일 주소(/api/files/…)에서만 통합니다. 그 밖의 경로에 키를 쓰면 401invalid_api_key입니다.
export MONSTRA_BASE="https://<api-host>/api/v1"
export MONSTRA_KEY="mk_live_…" # from the "API keys & webhook" screen
curl -s "$MONSTRA_BASE/me" -H "Authorization: Bearer $MONSTRA_KEY"2.API 키 만들기
로그인한 뒤 계정 메뉴의 "API 키·웹훅" 화면에서 키를 만듭니다. 키의 전체 값은 만들 때 한 번만 보입니다 — Monstra 는 해시만 저장하므로 다시 보여 줄 수 없습니다. 잃어버리면 새로 만들고 옛 키를 폐기하세요.
- 형식:
mk_live_+ 16진수 64자(모두 72자). 고정 접두어라 비밀 스캐너에 패턴을 등록하거나grep -r mk_live_로 저장소 유출을 점검하기 쉽습니다. - 키 하나는 그 계정의 웹 로그인과 같은 권한을 가집니다. 단, 결제·스토어 연결·키와 웹훅 설정은 웹에서만 됩니다.
- 폐기하면 그 즉시 401
invalid_api_key가 됩니다. 계정당 활성 키는 10개까지입니다. - 요청 한도는 키마다 분당 120, 그리고 계정 전체(모든 키 + 웹 로그인 토큰으로 부른 v1)로 분당 360 입니다. 넘으면 429
rate_limited와Retry-After(초)를 돌려줍니다. - 키 인증을 통과한 응답과 로그인 토큰으로 부른 v1 응답에는
X-RateLimit-Limit·X-RateLimit-Remaining·X-RateLimit-Reset(유닉스 초)이 붙습니다. 두 한도 중 한도에 더 가까운 쪽의 값이고,X-RateLimit-Scope(key또는user)가 어느 쪽인지 알려 줍니다. - 없는 키로 실패한 요청은 IP 당 분당 30번까지입니다. 넘으면 그 IP 의 키 요청이 잠시 429
rate_limited로 거절됩니다. - 크레딧 잔액·하루 생성 한도·동시 생성 수는 웹과 함께 씁니다.
3.처음부터 끝까지
상품 하나를 만들고, 사진을 링크로 넣고, 상품 소개를 생성해 HTML 을 받는 흐름입니다. 예제는 curl 과 jq 를 씁니다.
상품 만들기
external_id에 내 쪽 id 를 넣어 두면 나중에 그 값으로 상품을 찾을 수 있고, 같은 상품을 두 번 만들지 않습니다(409external_id_taken+product_id). 필드는 모두 생략할 수 있습니다.PRODUCT_ID=$(curl -s "$MONSTRA_BASE/products" \ -H "Authorization: Bearer $MONSTRA_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: ali-1005001-create" \ -d '{ "title": "Ceramic mug 350 ml", "description": "Supplier text, in any language", "external_id": "ali-1005001", "languages": ["EN", "KO"] }' | jq -r .product.id)사진을 링크로 넣기
요청 하나에 링크 20개까지. 바로 202 와 가져오기 작업(
import)을 돌려주고, 사진은 뒤에서 받아 옵니다. 파일을 직접 올리려면 "사진" 절의 multipart 예제를 쓰세요.IMPORT_ID=$(curl -s "$MONSTRA_BASE/products/$PRODUCT_ID/images/from-urls" \ -H "Authorization: Bearer $MONSTRA_KEY" \ -H "Content-Type: application/json" \ -d '{"urls": ["https://example.com/mug-1.jpg", "https://example.com/mug-2.jpg"]}' \ | jq -r .import.id)가져오기가 끝날 때까지 기다리기
status가completed가 되면 끝입니다. 링크마다items[].status가added또는failed(+code)입니다. 웹훅을 쓰면image_import.completed알림이 옵니다.while :; do IMPORT=$(curl -s "$MONSTRA_BASE/products/$PRODUCT_ID/image-imports/$IMPORT_ID" \ -H "Authorization: Bearer $MONSTRA_KEY") STATUS=$(echo "$IMPORT" | jq -r '.import.status // empty') case "$STATUS" in pending|running) sleep 3 ;; *) break ;; esac done echo "$STATUS" # completed, or empty when the call failed: see $IMPORT echo "$IMPORT" | jq '.import | {added, failed, items: [.items[] | {url, status, code}]}'상품 소개 생성
languages는 만들 언어(EN·KO·JA·ZH)입니다.Idempotency-Key를 붙이면 응답을 못 받아 다시 보내도 생성이 두 번 일어나지 않습니다. 202 와generation을 돌려줍니다.GENERATION_ID=$(curl -s "$MONSTRA_BASE/products/$PRODUCT_ID/compose" \ -H "Authorization: Bearer $MONSTRA_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: ali-1005001-compose-1" \ -d '{"languages": ["EN", "KO"]}' | jq -r .generation.id)생성이 끝날 때까지 기다리기
웹훅의
generation.completed·failed·cancelled를 받거나,GET /generations/{id}를 몇 초마다 확인합니다.status가pending·running이 아니면 끝난 것입니다. 언어별 결과는items에 있습니다.while :; do STATUS=$(curl -s "$MONSTRA_BASE/generations/$GENERATION_ID" \ -H "Authorization: Bearer $MONSTRA_KEY" | jq -r '.generation.status // empty') case "$STATUS" in pending|running) sleep 5 ;; *) break ;; esac done echo "$STATUS" # completed | failed | cancelled결과 받기
format=html은 HTML 조각을, 생략하면(json) 제목·설명·섹션·rendered_html·image_order를 줍니다. 페이지가 쓰는 사진 주소는 JSON 의image_order에 있고 약 1시간만 열리므로, 바로 내려받아 내 저장소에 올리세요.curl -s "$MONSTRA_BASE/products/$PRODUCT_ID/pages/EN?format=html" \ -H "Authorization: Bearer $MONSTRA_KEY" -o page-en.html # The page's photos (JSON image_order) are signed links that open for about an hour: keep copies. curl -s "$MONSTRA_BASE/products/$PRODUCT_ID/pages/EN" -H "Authorization: Bearer $MONSTRA_KEY" | jq -r '.image_order[]' | while read -r url; do curl -s -o "$(basename "$url" | cut -d'?' -f1)" "$url"; done
4.사진
- 파일 올리기:
POST /products/{id}/images에 multipartfiles파트를 여러 개 넣습니다. 같은 바이트의 사진은 새로 만들지 않고 이미 있는 사진(같은 id)을 돌려주므로, 실패하면 그냥 다시 올리면 됩니다. - 형식은 JPEG·PNG·WebP·AVIF 정지 이미지. 장당 10 MiB, 한 변 20000px·6400만 화소까지, 상품당 원본 50장까지입니다.
- 링크로 넣기:
POST /products/{id}/images/from-urls{"urls": […]}— 1–20개, 각 2048자 이하, 중복 불가, http/https. 공개 인터넷 주소만 됩니다 — 사설·내부 주소는 언제나 400image_url_invalid(+url)입니다. 리다이렉트는 3번까지 따라가고, 넘어간 주소도 같은 규칙으로 검사합니다. - 상품당 진행 중인 가져오기는 하나입니다(409
image_import_in_progress+import_id). 가져오는 동안에는 그 상품의 생성도 같은 코드로 거절됩니다. 계정 전체로는 동시에 5개 상품까지입니다(409too_many_image_imports). - 요청 때 바로 거절되는 경우: 지금 사진 수 + 링크 수가 50장을 넘으면 400
too_many_images, 저장 공간이 남지 않았으면 409storage_limit_reached. - 링크별 실패
code:private_address·invalid_url·redirect·http_status·too_large·timeout·network·unsupported_image·image_dimensions_too_large·too_many_images·storage_limit_reached. 일부가 실패해도 가져오기는completed로 끝납니다. - 응답의 사진 주소(
/api/files/…?e=…&s=…)는 서명된 주소로 약 1시간 열립니다. 내 사이트에 그대로 걸지 말고(핫링크 금지) 내려받아 보관하세요. 만료 뒤에는 401 이고, 다시 읽으면 새 주소를 받습니다. Idempotency-Key는 파일 올리기에는 쓸 수 없습니다(400). 위의 중복 처리 덕에 필요하지 않습니다.
curl -s "$MONSTRA_BASE/products/$PRODUCT_ID/images" \
-H "Authorization: Bearer $MONSTRA_KEY" \
-F "files=@mug-1.jpg" -F "files=@mug-2.jpg"5.웹훅
웹훅은 계정에 하나입니다. "API 키·웹훅" 화면에서 받을 주소(https)를 저장하면 서명용 비밀 값(mk_whsec_…)이 만들어집니다. 웹에서 시작했든 API 로 시작했든, 계정의 모든 작업이 끝날 때 알림이 갑니다. 알림에는 요약만 있으니 전체 내용은 받은 뒤 GET 으로 읽으세요. 웹훅 없이 GET 으로 확인하는 방법도 언제나 됩니다.
| 이벤트 | 언제 | data |
|---|---|---|
generation.completed · generation.failed · generation.cancelled | 생성이 끝났을 때(성공·실패·취소) | generation: id, product_id, external_id, kind, status, items[](language, status, error_message), error_message, cost, created_at, finished_at |
bulk.completed · bulk.cancelled | 일괄 작업이 끝났을 때 | bulk: GET /bulk/jobs/{id} 의 bulk 와 같음 |
store_job.completed · store_job.failed | 스토어 가져오기·반영·되돌리기가 끝났을 때 | job: id, kind, status, connection_id, product_id, external_id, error_code, error, cost, created_at, finished_at |
image_import.completed | 링크로 사진 가져오기가 끝났을 때 | import: GET …/image-imports/{id} 와 같음(항목 포함) |
ping | 화면에서 "테스트 알림 보내기"를 눌렀을 때 | {} |
POST /monstra/webhook HTTP/1.1
Content-Type: application/json
User-Agent: Monstra-Webhooks/1.0
Monstra-Event: generation.completed
Monstra-Delivery: 8123
Monstra-Signature: t=1760000000,v1=5f2b…(64 hex)
{
"id": 8123,
"event": "generation.completed",
"created_at": "2026-10-09T05:00:00.000Z",
"origin": "api",
"api_key_id": 7,
"data": {
"generation": {
"id": 912,
"product_id": 345,
"external_id": "ali-1005001",
"kind": "compose",
"status": "completed",
"items": [
{ "language": "EN", "status": "completed", "error_message": null },
{ "language": "KO", "status": "completed", "error_message": null }
],
"error_message": null,
"cost": 1830,
"created_at": "2026-10-09T04:58:41.000Z",
"finished_at": "2026-10-09T05:00:00.000Z"
}
}
}- 본문의
id는 전송 id 로Monstra-Delivery헤더와 같습니다. 재시도해도 바뀌지 않으니 이 값으로 중복을 거르세요. origin은"api"(API 키로 시작한 작업) 또는"web",api_key_id는 그 키의 id(웹이면null)입니다.- 상품을 지우면 그 상품의 생성·가져오기 알림은 오지 않습니다.
compose·translate요청 자체가 오류(5xx)로 끝나면 — 생성을 맡기는 단계에서 거절된 경우 —generation.failed알림은 오지 않습니다. 그 오류 응답이 결과입니다.
서명 확인
Monstra-Signature: t=<유닉스 초>,v1=<서명>— 서명은 비밀 값(mk_whsec_…문자열 전체)을 키로 한HMAC-SHA256(t + "." + 본문)의 16진수입니다.- 본문은 받은 바이트 그대로 씁니다. JSON 으로 파싱했다가 다시 직렬화하면 공백·순서가 달라져 서명이 맞지 않습니다.
t가 지금과 5분 넘게 차이 나면 거절하세요(재생 방지). 서명 비교는 상수 시간 비교(timingSafeEqual,hmac.compare_digest)로 합니다.t는 보낼 때마다 새로 정해집니다. 같은 알림의 재시도는 본문이 같고 서명만 다릅니다.- 비밀 값을 새로 만들면 그 순간부터(대기 중인 재시도까지) 새 값으로 서명합니다. 받는 쪽도 바로 바꾸세요.
import crypto from 'node:crypto'
import express from 'express'
const TOLERANCE_SECONDS = 5 * 60
/**
* rawBody: the request body exactly as received (a Buffer), never re-serialized JSON.
* header: the Monstra-Signature header. secret: the whole "mk_whsec_…" string.
*/
export function verifyMonstraSignature(rawBody, header, secret, nowSeconds = Date.now() / 1000) {
// A parsed body (express.json) or a missing secret can never verify.
if (!Buffer.isBuffer(rawBody) || !secret) return false
const parts = {}
for (const piece of String(header ?? '').split(',')) {
const i = piece.indexOf('=')
if (i > 0) parts[piece.slice(0, i).trim()] = piece.slice(i + 1).trim()
}
const t = parts.t
if (!/^\d+$/.test(t ?? '') || !/^[0-9a-f]{64}$/.test(parts.v1 ?? '')) return false
if (Math.abs(nowSeconds - Number(t)) > TOLERANCE_SECONDS) return false
const expected = crypto.createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest()
return crypto.timingSafeEqual(expected, Buffer.from(parts.v1, 'hex'))
}
const app = express()
// express.raw, not express.json: the signature is over the bytes as sent.
app.post('/monstra/webhook', express.raw({ type: 'application/json' }), (req, res) => {
if (!verifyMonstraSignature(req.body, req.get('Monstra-Signature'), process.env.MONSTRA_WEBHOOK_SECRET)) {
return res.sendStatus(400)
}
res.sendStatus(204) // answer first, work afterwards
const event = JSON.parse(req.body.toString('utf8'))
// De-duplicate on event.id (= Monstra-Delivery), then GET the current state.
console.log(event.event, event.id)
})
app.listen(3000)import hashlib
import hmac
import json
import os
import re
import time
from flask import Flask, request
TOLERANCE_SECONDS = 5 * 60
def verify_monstra_signature(raw_body: bytes, header: str, secret: str, now=None) -> bool:
"""raw_body: the request body exactly as received. secret: the whole "mk_whsec_..." string."""
parts = {}
for piece in (header or "").split(","):
key, sep, value = piece.partition("=")
if sep:
parts[key.strip()] = value.strip()
t, v1 = parts.get("t", ""), parts.get("v1", "")
if not re.fullmatch(r"[0-9]+", t) or not re.fullmatch(r"[0-9a-f]{64}", v1):
return False
if abs((time.time() if now is None else now) - int(t)) > TOLERANCE_SECONDS:
return False
expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1)
app = Flask(__name__)
@app.post("/monstra/webhook")
def monstra_webhook():
raw = request.get_data() # the bytes as sent, before any JSON parsing
if not verify_monstra_signature(raw, request.headers.get("Monstra-Signature", ""), os.environ["MONSTRA_WEBHOOK_SECRET"]):
return "", 400
event = json.loads(raw)
# Answer 2xx quickly; de-duplicate on event["id"] (= Monstra-Delivery) and do the work afterwards.
print(event["event"], event["id"])
return "", 204전송과 재시도
- 2xx 로 답하면 전달된 것입니다. 받자마자 2xx 로 답하고 실제 처리는 그 뒤에 하세요. 10초 안에 답하지 않으면 실패로 봅니다.
- 3xx 는 따라가지 않고 실패로 봅니다(서명된 본문을 다른 곳으로 보내지 않기 위해).
- 실패하면 1분 → 5분 → 30분 → 2시간 → 8시간 뒤에 다시 보냅니다(처음을 포함해 6번, 약 10시간 반). 그래도 안 되면 그 알림은
failed로 끝납니다. - 순서는 보장하지 않습니다.
created_at·id로 정렬하고, 최신 상태는 GET 으로 확인하세요. - 받는 주소는 공개 https 주소이고 기본 번호(443)만 됩니다 —
:8443·:80처럼 번호를 붙일 수 없습니다. 사설·내부 주소는 저장할 수 없습니다. - 웹훅을 끄거나 지우면 대기 중인 알림은 취소됩니다. 보내지 못하고 쌓인 알림이 1000건을 넘으면 오래된 것부터 취소됩니다.
- "API 키·웹훅" 화면에서 최근 알림 기록과 실패 이유를 볼 수 있습니다.
6.멱등성 키
응답을 받지 못했을 때 같은 요청을 안전하게 다시 보내려면 POST 에 Idempotency-Key 헤더를 붙이세요. 같은 키로 다시 보내면 처음 응답을 그대로 돌려줍니다.
- 모든 v1 POST 에 쓸 수 있습니다(파일 올리기 제외). PUT·DELETE 는 원래 여러 번 보내도 결과가 같으므로, 붙이면 400 으로 알려 줍니다.
- 값은 1–128자의
A–Z a–z 0–9 _ . : -. 계정 단위라 API 키를 바꿔도 이어지고, 24시간 보관합니다. - 다시 보낸 응답에는
Idempotency-Replayed: true가 붙습니다. 처음 응답을 재생하므로 상태(예: 생성의status)는 그때의 모습입니다 — 지금 상태는 GET 으로 확인하세요. - 같은 키로 다른 요청(경로나 본문이 다름)을 보내면 409
idempotency_key_reused, 첫 요청이 아직 처리 중이면 409idempotency_in_progress(잠시 뒤 다시)입니다. - 저장되어 재생되는 것은 요청 자체에 대한 답뿐입니다: 성공, 그리고 입력 오류·없음(404)·
external_id_taken·끝난 작업(generation_finished,bulk_finished) 같은 4xx. - 때에 따라 달라지는 답은 저장하지 않으므로, 같은 키로 다시 보내면 실제로 다시 실행됩니다: 모든 429·5xx, 그리고
too_many_active·quota_exceeded·insufficient_credit·busy·rate_limited·storage_limit_reached·product_limit_reached·too_many_image_imports·upload_in_progress·generation_in_progress·image_import_in_progress·store_push_in_progress·store_job_in_progress·bulk_in_progress·idempotency_in_progress·source_page_required·base_page_not_ready·store_needs_reauth·store_not_connected·store_unavailable·engine_unavailable·bulk_unavailable·writer_unavailable. - 다시 보낼 때는 같은 본문을 그대로(같은 바이트로) 보내세요. 경로 끝의
/는 무시합니다. 재생도 분당 요청 수에 셉니다. - 재생된 본문의 서명된 사진 주소는 이미 만료되었을 수 있습니다(약 1시간). 사진이 필요하면 GET 으로 다시 읽으세요.
curl -si "$MONSTRA_BASE/products/$PRODUCT_ID/compose" \
-H "Authorization: Bearer $MONSTRA_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: ali-1005001-compose-1" \
-d '{"languages": ["EN", "KO"]}' | grep -i '^idempotency-replayed'
# Idempotency-Replayed: true <- the first answer, replayed; no second generation7.내 쪽 id 연결 (external_id)
상품에 내 시스템의 id(상품 번호, SKU 등)를 붙여 두면 Monstra 의 상품 id 를 따로 저장하지 않아도 됩니다.
POST /products·PUT /products/{id}의external_id: 앞뒤 공백을 뺀 1–128자, 제어 문자 불가, 대소문자 구분. 계정 안에서 유일합니다.- 이미 쓰는 값이면 409
external_id_taken과 그 상품의product_id를 돌려줍니다 — 그 상품으로 이어서 작업하면 됩니다. - 찾기:
GET /products?external_id=…(정확히 일치, 0개 또는 1개). 다른 필터와 함께 쓸 수 있습니다. PUT은 상품 전체를 바꾸지만external_id는 예외입니다: 빼면 그대로 두고,null이면 지웁니다(languages도 빼면 그대로).- 생성·스토어 작업·사진 가져오기 알림에 상품의
external_id가 함께 옵니다.
curl -s "$MONSTRA_BASE/products?external_id=ali-1005001" \
-H "Authorization: Bearer $MONSTRA_KEY" | jq '.products[0].id'8.일괄 작업과 스토어
- 일괄 생성:
POST /bulk/generate/estimate로 견적을 본 뒤POST /bulk/generate. 일괄 삭제:POST /bulk/delete. 상태GET /bulk/jobs/{id}, 취소POST /bulk/jobs/{id}/cancel. - 요청 하나에 상품 100개까지, 계정당 진행 중인 일괄 작업은 하나입니다(409
bulk_in_progress+bulk_id). 끝나면bulk.completed·bulk.cancelled알림이 옵니다. - 일괄 작업은 상품을 하나씩 차례로 생성합니다. 여러 상품을 동시에 돌리려면 상품마다
compose를 직접 보내세요(동시 생성 한도 안에서). - 스토어 연결은 웹에서만 합니다. 연결한 뒤에는
GET /stores,POST /stores/{id}/import,GET /products/{id}/store/preview,POST /products/{id}/store/push·restore,POST /bulk/push를 API 로 쓸 수 있고, 끝나면store_job.completed·failed알림이 옵니다. - 스토어 연동이 꺼진 서비스에서는 스토어 경로가 404
not_found입니다.
9.한도
| 항목 | 값 | 넘으면 |
|---|---|---|
| 키별 요청 | 분당 120 | 429 rate_limited + Retry-After |
| 계정 전체 요청 | 분당 360 (모든 키 + 로그인 토큰으로 부른 v1) | 429 rate_limited + Retry-After |
| 없는 키로 실패한 요청 | IP 당 분당 30 | 429 rate_limited |
| 계정당 활성 키 | 10개 | 409 api_key_limit_reached |
| 동시 진행 생성 | 계정당 2 (관리자가 조정할 수 있음, /me 의 quota.max_active) | 429 too_many_active |
| 하루 생성 페이지 | 기본 1,000 (관리자가 계정별로 조정할 수 있고 없앨 수도 있음, /me 의 quota.daily_limit, null = 한도 없음) | 429 quota_exceeded |
| 크레딧 | 웹과 같은 잔액, 생성 중에는 예상 비용을 잡아 둠 | 429 insufficient_credit |
| 상품 수 / 저장 용량 | 500개 / 2 GiB | 409 product_limit_reached / storage_limit_reached |
| 사진 | 상품당 원본 50장, 장당 10 MiB, 한 변 20000px·6400만 화소 | 400 too_many_images · 413 file_too_large · 400 image_dimensions_too_large |
| 링크로 사진 넣기 | 요청당 20개, 링크 2048자, 상품당 진행 중 1개, 계정당 동시에 5개 상품 | 400 validation_failed · 409 image_import_in_progress · 409 too_many_image_imports |
| 일괄 작업 | 요청당 100개, 계정당 진행 중 1개 | 409 bulk_in_progress |
| JSON 본문 | 1 MiB | 413 payload_too_large |
| 서명된 사진 주소 | 약 1시간 — 내려받아 보관 | 만료 뒤 401 unauthorized |
| 웹훅 | 10초 안에 응답, 6번 시도, 기록 30일 | — |
| 웹훅 설정 (웹 화면) | 저장 분당 10번, 테스트 알림 분당 5번 | 429 rate_limited |
| 보내지 못한 웹훅 알림 | 1000건까지 대기 | 넘으면 오래된 것부터 취소 |
| 멱등성 키 | 24시간 보관 | 409 idempotency_key_reused · idempotency_in_progress |
10.오류 코드
자주 만나는 코드입니다. 경로별 응답은 OpenAPI 스펙에 있습니다.
| HTTP | code | 뜻 |
|---|---|---|
| 400 | validation_failed · invalid_json | 입력값이나 JSON 이 올바르지 않음. error 에 어느 필드인지 적혀 있습니다. |
| 400 | images_required · languages_required | 생성하려는데 사진이 없거나 만들 언어가 비었음 |
| 400 | no_files · invalid_upload · too_many_images · unsupported_image · image_dimensions_too_large · invalid_image_order | 사진 올리기·순서 오류 |
| 400 | image_url_invalid (+url) | 쓸 수 없는 사진 링크(형식, 사설·내부 주소) |
| 400 | writer_unavailable | 오류가 발생해서 지금은 페이지를 만들 수 없음 — 잠시 뒤 다시 시도 |
| 401 | unauthorized | 인증 없음, 또는 만료된 서명 주소 |
| 401 | invalid_api_key | 틀렸거나 폐기된 키, 또는 v1 이 아닌 경로에 키를 씀 |
| 403 | browser_not_allowed | 브라우저(Origin 헤더)에서 키를 씀 |
| 404 | not_found · product_not_found · image_not_found · page_not_found · generation_not_found · image_import_not_found · bulk_not_found · store_not_found · store_job_not_found | 없거나 내 것이 아님(남의 것도 404) |
| 409 | generation_in_progress | 그 상품이 이미 생성 중 |
| 409 | source_page_required | 번역하려면 먼저 생성(compose)이 필요 |
| 409 | image_import_in_progress (+import_id) | 사진을 링크에서 가져오는 중 |
| 409 | too_many_image_imports | 계정 전체에서 사진 가져오기 5개가 이미 진행 중 |
| 409 | external_id_taken (+product_id) | 이미 쓰는 external_id |
| 409 | idempotency_key_reused · idempotency_in_progress | 멱등성 키 충돌 |
| 409 | product_limit_reached · storage_limit_reached | 상품 수·저장 용량 한도 |
| 409 | bulk_in_progress (+bulk_id) · store_job_in_progress (+job_id) | 이미 진행 중인 일괄·스토어 작업 |
| 413 | file_too_large · payload_too_large | 사진 10 MiB, JSON 1 MiB 초과 |
| 429 | rate_limited | 분당 요청 수 초과(키·계정, 또는 없는 키 실패가 잦은 IP) — Retry-After 초 뒤에 다시 |
| 429 | too_many_active · quota_exceeded · insufficient_credit | 동시 생성 수·하루 한도·크레딧 부족 |
| 429 | upload_in_progress | 같은 계정의 다른 업로드가 진행 중 |
| 500 | internal_error | 예상하지 못한 오류 — 잠시 뒤 다시 시도 |
| 502 · 503 | engine_rejected · engine_error · engine_unavailable · busy | 생성 쪽이 잠시 응답하지 않음 — 잠시 뒤 다시 시도 |
| 400 · 404 · 409 | webhook_url_invalid · webhook_not_found · api_key_not_found · api_key_limit_reached | 웹 화면 전용(키·웹훅 설정) — /api/v1 에서는 나오지 않음 |
11.OpenAPI 스펙
모든 경로와 요청·응답 모양은 OpenAPI 3.1 스펙에 있습니다. 코드 생성기나 Postman 에 바로 불러올 수 있습니다. 응답 스키마는 additionalProperties: false 로 적혀 있지만, 클라이언트는 새 필드를 무시하도록 만드세요.
openapi.json 열기https://<api-host>/api/v1/openapi.json
# Account and catalog
GET /me
GET /me/ledger?cursor=&limit=
GET /languages
GET /personas
GET /layouts?lang=
# Products
GET /products?limit=&offset=&q=&sort=&source=&state=&lang=&lang_state=&external_id=
POST /products
GET /products/{id}
PUT /products/{id}
DELETE /products/{id}
PUT /products/{id}/layout
PUT /products/{id}/image-selection
PUT /products/{id}/photo-set
PUT /products/{id}/hero-image
# Photos
POST /products/{id}/images (multipart "files")
POST /products/{id}/images/from-urls
GET /products/{id}/image-imports/{importId}
PUT /products/{id}/images/order
DELETE /products/{id}/images/{imageId}
# Generation and pages
POST /products/{id}/compose
POST /products/{id}/translate
GET /products/{id}/generations?limit=
GET /generations/{id}
POST /generations/{id}/cancel
GET /products/{id}/pages/{language}?format=json|html
GET /products/{id}/pages/{language}/history
GET /products/{id}/source
# Bulk
POST /bulk/delete
POST /bulk/generate/estimate
POST /bulk/generate
GET /bulk/jobs?active=&limit=
GET /bulk/jobs/{id}
POST /bulk/jobs/{id}/cancel
POST /bulk/push/plan
POST /bulk/push
# Stores (connected on the web first)
GET /stores
GET /stores/jobs/{jobId}
GET /stores/{id}/products?cursor=&q=
POST /stores/{id}/import
GET /products/{id}/store/preview
POST /products/{id}/store/push
POST /products/{id}/store/restore
# Docs
GET /openapi.json (public)12.버전 정책
- v1 은 필드와 경로를 더하기만 합니다. 클라이언트는 모르는 응답 필드를 무시해야 합니다.
- 이름 바꾸기·삭제·타입이나 뜻 바꾸기는 v2 로 냅니다.
- 요청의 모르는 필드는 400
validation_failed입니다. /api/v1이 아닌/api/…경로는 웹 화면 전용이라 예고 없이 바뀝니다. 프로그램에서는 쓰지 마세요.error문장은 한국어이며 바뀔 수 있습니다. 분기는code로 하세요.