개발자

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 헤더가 붙은 요청(브라우저)은 403 browser_not_allowed 로 거절됩니다.
  • API 키는 /api/v1 경로와 응답 안의 서명된 파일 주소(/api/files/…)에서만 통합니다. 그 밖의 경로에 키를 쓰면 401 invalid_api_key 입니다.
내 계정 확인 (GET /me)
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 를 씁니다.

  1. 상품 만들기

    external_id 에 내 쪽 id 를 넣어 두면 나중에 그 값으로 상품을 찾을 수 있고, 같은 상품을 두 번 만들지 않습니다(409 external_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)

  2. 사진을 링크로 넣기

    요청 하나에 링크 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)

  3. 가져오기가 끝날 때까지 기다리기

    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}]}'

  4. 상품 소개 생성

    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)

  5. 생성이 끝날 때까지 기다리기

    웹훅의 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

  6. 결과 받기

    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 에 multipart files 파트를 여러 개 넣습니다. 같은 바이트의 사진은 새로 만들지 않고 이미 있는 사진(같은 id)을 돌려주므로, 실패하면 그냥 다시 올리면 됩니다.
  • 형식은 JPEG·PNG·WebP·AVIF 정지 이미지. 장당 10 MiB, 한 변 20000px·6400만 화소까지, 상품당 원본 50장까지입니다.
  • 링크로 넣기: POST /products/{id}/images/from-urls {"urls": […]} — 1–20개, 각 2048자 이하, 중복 불가, http/https. 공개 인터넷 주소만 됩니다 — 사설·내부 주소는 언제나 400 image_url_invalid(+url)입니다. 리다이렉트는 3번까지 따라가고, 넘어간 주소도 같은 규칙으로 검사합니다.
  • 상품당 진행 중인 가져오기는 하나입니다(409 image_import_in_progress + import_id). 가져오는 동안에는 그 상품의 생성도 같은 코드로 거절됩니다. 계정 전체로는 동시에 5개 상품까지입니다(409 too_many_image_imports).
  • 요청 때 바로 거절되는 경우: 지금 사진 수 + 링크 수가 50장을 넘으면 400 too_many_images, 저장 공간이 남지 않았으면 409 storage_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). 위의 중복 처리 덕에 필요하지 않습니다.
파일 올리기 (multipart)
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 는 보낼 때마다 새로 정해집니다. 같은 알림의 재시도는 본문이 같고 서명만 다릅니다.
  • 비밀 값을 새로 만들면 그 순간부터(대기 중인 재시도까지) 새 값으로 서명합니다. 받는 쪽도 바로 바꾸세요.
Node.js (Express)
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)

Python (Flask)
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, 첫 요청이 아직 처리 중이면 409 idempotency_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 generation

7.내 쪽 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.한도

항목값넘으면
키별 요청분당 120429 rate_limited + Retry-After
계정 전체 요청분당 360 (모든 키 + 로그인 토큰으로 부른 v1)429 rate_limited + Retry-After
없는 키로 실패한 요청IP 당 분당 30429 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 GiB409 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 MiB413 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 스펙에 있습니다.

HTTPcode뜻
400validation_failed · invalid_json입력값이나 JSON 이 올바르지 않음. error 에 어느 필드인지 적혀 있습니다.
400images_required · languages_required생성하려는데 사진이 없거나 만들 언어가 비었음
400no_files · invalid_upload · too_many_images · unsupported_image · image_dimensions_too_large · invalid_image_order사진 올리기·순서 오류
400image_url_invalid (+url)쓸 수 없는 사진 링크(형식, 사설·내부 주소)
400writer_unavailable오류가 발생해서 지금은 페이지를 만들 수 없음 — 잠시 뒤 다시 시도
401unauthorized인증 없음, 또는 만료된 서명 주소
401invalid_api_key틀렸거나 폐기된 키, 또는 v1 이 아닌 경로에 키를 씀
403browser_not_allowed브라우저(Origin 헤더)에서 키를 씀
404not_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)
409generation_in_progress그 상품이 이미 생성 중
409source_page_required번역하려면 먼저 생성(compose)이 필요
409image_import_in_progress (+import_id)사진을 링크에서 가져오는 중
409too_many_image_imports계정 전체에서 사진 가져오기 5개가 이미 진행 중
409external_id_taken (+product_id)이미 쓰는 external_id
409idempotency_key_reused · idempotency_in_progress멱등성 키 충돌
409product_limit_reached · storage_limit_reached상품 수·저장 용량 한도
409bulk_in_progress (+bulk_id) · store_job_in_progress (+job_id)이미 진행 중인 일괄·스토어 작업
413file_too_large · payload_too_large사진 10 MiB, JSON 1 MiB 초과
429rate_limited분당 요청 수 초과(키·계정, 또는 없는 키 실패가 잦은 IP) — Retry-After 초 뒤에 다시
429too_many_active · quota_exceeded · insufficient_credit동시 생성 수·하루 한도·크레딧 부족
429upload_in_progress같은 계정의 다른 업로드가 진행 중
500internal_error예상하지 못한 오류 — 잠시 뒤 다시 시도
502 · 503engine_rejected · engine_error · engine_unavailable · busy생성 쪽이 잠시 응답하지 않음 — 잠시 뒤 다시 시도
400 · 404 · 409webhook_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 로 하세요.