온라인 문패 · API 가이드
고객과 컨설턴트를 위한 문패 API
사업 소개를 보내 AI 시안을 만들고, 완성된 내용을 홈페이지에 반영할 수 있어요. API는 다른 프로그램에서 문패의 기능을 호출하는 방법이에요. 카페의 메뉴 소개나 공방의 주문 안내처럼, 만들고 싶은 내용을 먼저 준비해 주세요.
코드 없이 시작하려면 시안 만들기를 이용하세요. 직접 연동하는 개발자는 아래 순서대로 따라오면 돼요.
1. 시안을 만들까요, 홈페이지를 수정할까요?
처음 만드는 홈페이지라면 무료 시안부터 요청하세요. 이미 운영하는 홈페이지의 내용을 바꾸려면 해당 고객사의 API 키를 준비하면 돼요.
시안 만들기 + 상담 접수
POST /api/v1/drafts
사업 소개·연락처·상담 연락 동의가 필요해요. API 키는 없어도 돼요.
현재 홈페이지 읽기
GET /api/v1/landing-page
고객사 주소와 API 키를 준비하세요.
홈페이지 내용 바꾸기
PUT /api/v1/landing-page
고객사 주소·API 키·교체할 내용이 필요해요.
시안 생성은 공개 홈페이지 발행과 별개예요. 먼저 미리보기를 확인한 뒤 발행해 주세요. 랜딩페이지 PUT은 운영 중인 홈 내용을 바로 교체해요.
누가 어떤 키로 호출하나요?
고객은 자기 사이트를 관리하고, 컨설턴트는 위임받은 고객 사이트를 작업합니다. 키는 서버에서 HTTPS의 X-Api-Key 헤더로 전달하세요. 문서는 로그인 없이 읽을 수 있지만 키를 브라우저 코드나 업로드 파일에 넣으면 안 됩니다.
| 호출자 | 사용 범위 | 요청 호스트 |
|---|---|---|
| 익명 사용자 | 무료 시안 POST /api/v1/drafts. 소유자 쿠키·동의·한도 적용 | 허용된 문패 호스트 |
| 고객 API 키 | 랜딩·블로그·문의·고객 웹훅 | 키를 발급한 고객사의 검증된 활성 호스트 |
| 파일 발행 권한이 있는 고객 키 | 기본 고객 API + /api/v1/site 및 하위 경로 | 해당 고객 사이트 호스트 (본사 제외) |
| 컨설턴트 키 | /api/consultant/v1/sites 생성·목록 | munpae.kr |
| 연결된 컨설턴트 키 | /api/v1/site 및 하위 경로. 일반 고객 API 권한과 구분 | 작업 연결이 활성인 고객 사이트 호스트 |
고객 키는 API 키 관리에서 발급합니다. 파일 API를 사용하려면 파일 발행 권한을 체크한 새 키가 필요하며 기존 키 권한은 자동 확대되지 않습니다. 컨설턴트 키는 플랫폼 운영자가 발급합니다. 두 키 모두 90일 후 만료되고, 폐기된 키는 새 요청에 사용할 수 없습니다. 로그인 쿠키는 API 키를 대체하지 않습니다.
curl "$MUNPAE_SITE_URL/api/v1/site" \
-H "X-Api-Key: $MUNPAE_SITE_API_KEY"위 환경 변수에는 고객 사이트의 HTTPS 주소와 파일 발행 가능한 키를 설정합니다. 고객은 자기 주소를, 컨설턴트는 생성 응답의 URL 또는 연결된 고객 주소를 사용합니다.
2. 고객에게 안내하고 시안을 요청하세요
예를 들어 카페 홈페이지를 만든다면 이름, 연락처, 카페 이름, 한줄 소개를 준비해요. 신청자에게 아래 내용을 보여 주고 직접 동의를 받은 뒤 요청하세요. 연동 프로그램이 동의를 대신 선택하면 안 돼요.
요청할 내용을 JSON으로 정리해요
JSON은 이름과 값을 짝지어 보내는 형식이에요. 아래는 가상 카페 예시예요. 실제 입력과 동의로 바꾼 뒤 보내세요.
{
"name": "예시 고객",
"phone": "010-0000-0000",
"businessName": "문패 베이커리",
"introduction": "매일 아침 크루아상을 굽는 동네 빵집입니다.",
"privacyConsent": true,
"prompt": "대표 메뉴와 방문 안내를 따뜻하게 소개해 주세요.",
"style": "따뜻하고 친근한",
"theme": "오렌지"
}
요청을 보내고 미리보기를 확인해요
터미널에서 실행하는 cURL 예시예요. 정상 처리되면 201 Created와 draft.previewUrl을 받아요. 요청할 때 사용한 쿠키를 유지해야 미리보기를 볼 수 있어요.
# 실제 무료 생성 횟수를 사용합니다. 본인 정보와 실제 동의로 교체하세요.
# 쿠키 파일은 비밀 정보입니다. 공유하거나 저장소에 올리지 마세요.
# 먼저 쿠키 확보. REQUEST_ID는 요청마다 새 UUID, 재시도에는 같은 값을 유지하세요.
curl --proto '=https' --cookie munpae-cookies.txt --cookie-jar munpae-cookies.txt \
--output /dev/null 'https://munpae.kr/Draft/New'
REQUEST_ID='11111111-1111-4111-8111-111111111111'
curl --proto '=https' --fail-with-body --include 'https://munpae.kr/api/v1/drafts' \
--cookie munpae-cookies.txt --cookie-jar munpae-cookies.txt \
--header 'Content-Type: application/json' \
--header "Idempotency-Key: $REQUEST_ID" \
--data-raw '{
"name": "예시 고객",
"phone": "010-0000-0000",
"businessName": "문패 베이커리",
"introduction": "매일 아침 크루아상을 굽는 동네 빵집입니다.",
"privacyConsent": true,
"prompt": "대표 메뉴와 방문 안내를 따뜻하게 소개해 주세요.",
"style": "따뜻하고 친근한",
"theme": "오렌지"
}'
시안 생성이 성공하면 신규 상담도 함께 접수돼요. 문패 운영팀은 /Platform/Leads의 ‘신규 상담’에서 API 유입과 동의 시각을 확인해요. #문패_신규에는 접수 알림과 관리자 상세 링크가 전달돼요. 고객사 사이트의 일반 ‘문의 관리’에는 추가되지 않아요.
응답을 못 받았다면 같은 요청으로 다시 확인해요
Idempotency-Key는 같은 요청인지 구분하는 중복 방지 번호예요. 요청마다 UUID를 하나 만들고, 재시도할 때는 같은 쿠키·같은 키·같은 내용을 유지하세요. 저장이 끝난 요청은 같은 시안을 돌려주고 생성 횟수·상담·알림을 추가하지 않아요. 응답 헤더 Idempotency-Replayed: true로 확인할 수 있어요.
이 헤더는 선택 사항이에요. 생략하거나 쿠키·키를 바꾸면 새 요청이므로 중복 방지가 적용되지 않아요. 익명 연동은 먼저 GET /Draft/New으로 쿠키를 확보하고 보관하세요. 최초 응답과 쿠키를 모두 잃으면 이전 요청을 식별할 수 없어요. 로그인 전후 계정이 바뀐 경우도 같은 소유자로 처리되지 않아요.
개발자 상세: 입력 필드, 응답, JavaScript와 사용량 제한
- 기본 주소는
https://munpae.kr입니다. HTTPS로 요청하며Content-Type: application/json인 UTF-8 JSON 객체를 보냅니다. 본문 최대 크기는 16 KiB (16,384바이트)이며 스트리밍 본문도 제한됩니다. - 아래 camelCase 필드를 본문 최상위에 보냅니다.
{ "input": ... }래퍼가 아닙니다. 알 수 없는 필드는400 invalid_json으로 거부됩니다.tenantId,userId, API 키, AI 공급자 키를 본문에 넣지 마세요. - 익명 요청은 쿠키로 소유자를 식별하고, 웹 로그인 상태에서는 로그인 계정을 사용합니다. API 키로 소유자나 무료 한도를 바꿀 수 없습니다. 브라우저 예시는 같은 출처에서만 실행하며 외부 사이트용 CORS를 개방하지 않습니다.
복사 버튼은 텍스트만 복사하며 요청을 실행하지 않습니다. 무료 시안 예시를 직접 실행하면 실제 AI 생성과 무료 횟수 사용이 발생합니다. 가상 예시 정보는 본인 정보로 바꾸고 개인정보 안내를 확인한 뒤 실제 동의가 있을 때만 privacyConsent: true로 보내세요.
POST /api/v1/drafts — 홈페이지 신청폼의 필수 5개 필드에 선택 3개를 추가할 수 있습니다. 웹 시안 만들기와 생성 서비스·무료 한도를 공유하지만, 웹 폼과 API의 입력 형식은 다릅니다.
| JSON 필드 | 자료형 · 필수 여부 | 입력 규칙 / 홈페이지 대응 |
|---|---|---|
name | string · 필수 | 이름. 공백 제거 후 1~100자. |
phone | string · 필수 | 연락처. 국내 휴대폰/지역번호 형식, 하이픈 선택. 예: 010-0000-0000, 02-000-0000. 숫자가 아닌 문자열입니다. |
businessName | string · 필수 | 사업장명. 공백 제거 후 1~200자. |
introduction | string · 필수 | 한줄 소개. 공백 제거 후 1~500자. |
privacyConsent | boolean · 필수 | 개인정보 수집 및 연락 안내 동의. 따옴표 없는 true만 허용. false/생략은 검증 오류입니다. |
prompt | string 또는 null · 선택 | 추가 요청. 생략/null 허용, 값이 있으면 공백 제거 후 5~2,000자. 빈 문자열/공백만 보내면 오류입니다. |
style | string 또는 null · 선택 | 분위기. 공백 제거 후 최대 100자. 생략/null/빈 문자열 허용. 고정 enum이 아닙니다. |
theme | string 또는 null · 선택 | 테마 색상. 공백 제거 후 최대 100자. 생략/null/빈 문자열 허용. 고정 enum이 아닙니다. |
문자열은 앞뒤 공백을 제거합니다. 전화번호는 ^(01[0-9]-?\d{3,4}-?\d{4}|0\d{1,2}-?\d{3,4}-?\d{4})$로 검증하며 국제번호 형식은 지원하지 않습니다. 선택 필드 생략 시 응답에는 null이 반환됩니다. 스타일·색상에 고정 기본값을 넣지 않습니다.
{
"name": "예시 고객",
"phone": "010-0000-0000",
"businessName": "문패 베이커리",
"introduction": "매일 아침 크루아상을 굽는 동네 빵집입니다.",
"privacyConsent": true
}
소유자 쿠키 · 웹과 합산 3회
익명 사용자는 munpae_draft_device, 로그인 사용자는 웹 계정으로 소유자를 판별합니다. 웹/API 생성 및 재생성은 같은 소유자의 무료 생성 3회를 공유합니다. 첫 성공 생성부터 3시간의 고정 창으로 계산하고 경계 시각부터 다시 3회 생성할 수 있습니다. 가입·로그인으로 귀속된 성공 기록도 합산하며 가입만으로 초기화되지 않습니다. 생성 실패와 동일 중복 방지 키 재전송은 추가 차감하지 않습니다.
익명 소유자 쿠키와 성공 시 발급되는 munpae_draft_id 쿠키는 HttpOnly·Secure·SameSite=Lax·30일입니다. 쿠키 파일을 계속 유지하세요. cURL에서 받은 URL만 쿠키가 없는 다른 브라우저에 붙여넣으면 미리보기를 볼 수 없습니다. 로그인 상태에서도 API 키는 필요하지 않습니다.
요청 속도 제한은 프로세스별 연결 IP 기준 1분에 10회이며, 프록시 뒤에서는 여러 사용자가 같은 버킷을 공유할 수 있습니다. 전역 분산 IP 제한이 아닙니다. 동일 프롬프트 반복만으로 영구 차단하지 않습니다. 쿠키 교체로 제한을 우회하지 마세요.
201 Created · request + draft + quota
응답의 request는 정규화된 8개 입력 필드입니다. draft는 id(UUID), prompt, style, theme, content, createdAt(ISO 8601), previewUrl을 포함합니다. draft.prompt는 사업장명·한줄 소개·선택 추가 요청을 합친 AI 입력입니다.
quota에는 limit, used, remaining, resetsAt(UTC ISO 8601, 활성 창이 없으면 null), retryAfterSeconds가 포함됩니다. 무료 한도 429에도 같은 정보를 반환하며 Retry-After는 초기화까지 남은 초입니다. 첫 생성은 성공 저장 시각을 기준으로 하며, 생성 도중 이전 창이 만료되면 새 창에 기록됩니다. 소유자 쿠키는 물리 기기 식별을 보장하지 않습니다.
{
"request": {
"prompt": "대표 메뉴와 방문 안내를 따뜻하게 소개해 주세요.",
"style": "따뜻하고 친근한",
"theme": "오렌지",
"name": "예시 고객",
"phone": "010-0000-0000",
"businessName": "문패 베이커리",
"introduction": "매일 아침 크루아상을 굽는 동네 빵집입니다.",
"privacyConsent": true
},
"quota": {
"limit": 3,
"used": 1,
"remaining": 2,
"resetsAt": "2026-09-07T12:00:00+00:00",
"retryAfterSeconds": 0
},
"draft": {
"id": "11111111-1111-4111-8111-111111111111",
"prompt": "사업장명: 문패 베이커리\n한줄 소개: 매일 아침 크루아상을 굽는 동네 빵집입니다.\n추가 요청: 대표 메뉴와 방문 안내를 따뜻하게 소개해 주세요.",
"style": "따뜻하고 친근한",
"theme": "오렌지",
"content": {
"sections": [
{
"type": "hero",
"title": "문패 베이커리",
"subtitle": "매일 아침 굽는 크루아상",
"cta": "방문 문의"
},
{
"type": "about",
"title": "우리 이야기",
"body": "동네에서 만나는 작은 빵집입니다."
},
{
"type": "contact",
"title": "신청 · 문의",
"body": "궁금한 점이나 신청 내용을 남겨 주세요."
}
],
"brandColor": "#C99550",
"logoUrl": null,
"copy": {
"tagline": "동네에서 만나는 따뜻한 빵집"
}
},
"createdAt": "2026-09-07T09:00:00+00:00",
"previewUrl": "/Draft/Preview/11111111-1111-4111-8111-111111111111"
}
}
draft.content는 JSON 객체입니다. JSON 문자열인 contentJson 필드는 없으며 추가 JSON.parse가 필요하지 않습니다. sections는 섹션 배열, brandColor는 색상, logoUrl은 선택 URL, copy는 문구 사전입니다. 섹션에는 type 및 선택 title/subtitle/body/cta/imageUrl/items(문자열 배열)가 들어갑니다. AI 결과의 문구와 구성을 확인하세요. 신규 시안에는 신청·문의 폼(contact)이 항상 포함되며, 발행 후 해당 고객사의 문의 관리로 접수됩니다.
{
"sections": [
{ "type": "hero", "title": "문패 베이커리", "subtitle": "매일 아침 굽는 크루아상", "cta": "방문 문의" },
{ "type": "about", "title": "우리 이야기", "body": "동네에서 만나는 작은 빵집입니다." },
{ "type": "contact", "title": "신청 · 문의", "body": "궁금한 점이나 신청 내용을 남겨 주세요." }
],
"brandColor": "#C99550",
"logoUrl": null,
"copy": { "tagline": "동네에서 만나는 따뜻한 빵집" }
}
// 실제 생성 요청입니다. 입력과 동의를 확인한 뒤 한 번만 실행하세요.
const input = {
"name": "예시 고객",
"phone": "010-0000-0000",
"businessName": "문패 베이커리",
"introduction": "매일 아침 크루아상을 굽는 동네 빵집입니다.",
"privacyConsent": true,
"prompt": "대표 메뉴와 방문 안내를 따뜻하게 소개해 주세요.",
"style": "따뜻하고 친근한",
"theme": "오렌지"
};
// 재시도에는 같은 requestId와 input을 유지하세요.
await fetch('/Draft/New', { credentials: 'same-origin' });
const requestId = crypto.randomUUID();
const response = await fetch('/api/v1/drafts', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json', 'Idempotency-Key': requestId },
body: JSON.stringify(input)
});
const isJson = response.headers.get('content-type')?.includes('json');
const result = isJson ? await response.json() : null;
if (!response.ok) {
// 429의 code와 Retry-After를 구분합니다. 자동 재시도하지 않습니다.
const reason = result?.code ?? (result?.errors ? 'validation_failed' : 'request_failed');
throw new Error(`HTTP ${response.status}: ${reason}; Retry-After=${response.headers.get('retry-after') ?? '없음'}`);
}
const { draft } = result;
// draft.content는 이미 객체입니다. JSON.parse를 다시 호출하지 않습니다.
const content = draft.content;
// 응답의 request에는 개인정보가 있으므로 콘솔/분석 도구에 기록하지 않습니다.
// 같은 브라우저 쿠키를 유지해야 미리보기에 접근할 수 있습니다.
location.assign(draft.previewUrl);
Location 헤더와 draft.previewUrl은 같은 /Draft/Preview/{id} 웹 화면을 가리킵니다. 소유자 쿠키 또는 해당 로그인 계정을 유지해야 합니다. 시안 조회용 GET API는 없습니다. 시안 생성은 사이트 발행이 아니며, 미리보기에서 내용을 확인한 뒤 별도 발행 절차를 진행하세요.
응답에는 이름·연락처·동의 정보가 포함되므로 로그/분석 도구에 전체 응답을 남기지 마세요. 응답은 Cache-Control: no-store입니다. 이름·연락처 필드는 자동으로 AI 프롬프트에 포함하지 않지만, 사업장명·소개·추가 요청·스타일·테마에 직접 넣은 개인정보는 AI에 전달될 수 있습니다. 시안 생성에 성공하면 동의한 연락처를 문패 운영팀의 신규 상담에 등록합니다. Slack에는 접수 알림과 관리자 상세 링크만 보내며 이름·연락처·미리보기 주소는 보내지 않습니다.
3. 홈페이지·블로그·문의를 관리하세요
고객사 홈페이지 주소로 요청하세요. GET으로 현재 내용을 읽고 백업한 다음, PUT으로 제목과 전체 내용을 바꿀 수 있어요. 이 과정은 AI 시안이나 신규 상담을 만들지 않아요.
munpae.kr 서비스 홈페이지에 PUT 테스트하지 마세요. 승인된 테스트 테넌트, 즉 별도로 준비한 고객사 테스트 사이트에서 확인하세요. PUT은 저장 즉시 공개되며 기존 내용을 전체 교체해요.
# 서버 전용. 호스트는 승인된 테스트 테넌트로만 교체하세요. munpae.kr 금지.
# MUNPAE_API_KEY는 서버 환경 변수/비밀 저장소에서 설정합니다. 출력하지 마세요.
# HTTP로 보내거나 --location/-L로 리다이렉트를 따라가지 마세요.
curl --proto '=https' --max-redirs 0 --fail-with-body --include \
--request GET 'https://authorized-test-tenant.example.invalid/api/v1/landing-page' \
--header "X-Api-Key: ${MUNPAE_API_KEY:?Set a server-only test tenant key}"
개발자 상세: PUT 예시, 전체 입력 규칙과 저장 결과
GET /api/v1/landing-page와 PUT /api/v1/landing-page는 요청 호스트에 연결된 검증 완료·활성 고객사의 slug: home 한 페이지만 다룹니다. URL이나 본문으로 tenantId/pageId/slug를 선택하지 않습니다. 웹 로그인 쿠키는 API 키의 대안이 아닙니다. HTTPS와 단일 X-Api-Key 헤더가 필요하며 쿼리·본문·Bearer·쿠키로 키를 보내지 않습니다.
운영 서비스 홈페이지 munpae.kr / www.munpae.kr에는 PUT 테스트 금지. 예시의 .invalid 호스트는 의도적으로 실행 불가한 자리표시자입니다. 쓰기 권한과 변경 승인을 받은 별도 테스트 테넌트 호스트로만 교체하세요. 키는 서버 환경 변수/비밀 저장소에만 보관하고 브라우저·프런트엔드에 배포하지 마세요. HTTP로 보내고 HTTPS 리다이렉트에 맡기면 이미 키가 노출됩니다. cURL의 --location/-L을 추가하지 말고 JavaScript의 redirect: 'error'를 유지하세요.
GET · 읽기 전용 및 키 확인
GET은 본문 없이 호출합니다. 기존 home이 있으면 200, 없으면 404이며 생성하거나 복구하지 않습니다. 알려진 검증·활성 호스트에서 유효한 키로 GET했어도 home이 없으면 404입니다. 미들웨어의 호스트 404와 구분해야 하므로 404만으로 키 유효성을 확정하지 마세요. GET은 기존 contentJson 원문을 그대로 반환합니다. 잘못된 레거시 JSON도 200으로 읽을 수 있으며 새 PUT 규칙을 충족한다는 보장은 없습니다. 원문을 백업하고 명시적으로 수정한 뒤 PUT으로 복구하세요.
// 승인된 테스트 테넌트로만 호스트를 교체하세요. 서비스 홈페이지 금지.
const apiKey = process.env.MUNPAE_API_KEY;
if (!apiKey) throw new Error('Server-only test tenant key required');
const response = await fetch('https://authorized-test-tenant.example.invalid/api/v1/landing-page', {
method: 'GET', redirect: 'error', credentials: 'omit',
headers: { 'X-Api-Key': apiKey }
});
// 알려진 활성·검증 호스트의 404는 유효 키 + home 없음일 수 있습니다.
// 미확인 호스트의 미들웨어 404만으로 키가 유효하다고 판단하지 마세요.
if (!response.ok) throw new Error(`GET HTTP ${response.status}`);
const page = await response.json();
const rawContentJson = page.contentJson; // 문자열. GET은 손상된 기존 JSON도 그대로 반환합니다.
// 필요한 경우 try/catch로 JSON.parse 후 별도 검증하세요. 전체 응답/키는 로깅하지 않습니다.
PUT 요청 스키마 · contentJson은 객체가 아닌 STRING
최상위 JSON 객체에는 필수 title과 contentJson 문자열만 보냅니다. contentJson 안에는 아래 객체를 JSON 문자열로 직렬화하여 넣습니다. 무료 시안의 draft.content 객체와 다릅니다. AI 결과를 옮길 때도 검증·수정 후 JSON.stringify(draft.content)로 문자열을 만드세요.
| 필드 | 자료형 · 필수 여부 | 규칙 |
|---|---|---|
title | string · 필수 | null 불가, 앞뒤 공백 제거 후 1~200자. 저장 시 제목만 trim합니다. |
contentJson | string · 필수 | null 불가, 1~100,000자. 내부는 JSON 객체이며 원문 문자열을 그대로 저장합니다. |
sections | object[] · 내부 필수 | 비어 있지 않은 배열. null 배열·null 항목 불가. 최소 1개이며 모든 종류를 넣을 필요는 없습니다. |
sections[].type | string · 필수 | 소문자 hero / about / services / portfolio / contact 다섯 종류만 허용. Hero 등은 거부합니다. |
sections[].title / subtitle / body / cta | string 또는 null · 선택 | 생략·null·빈 문자열 허용. HTML 실행용 필드가 아니며 공개 렌더러는 텍스트를 이스케이프합니다. |
sections[].imageUrl / logoUrl | string 또는 null · 선택 | 생략/null로 이미지 제거. 빈 문자열 불가. 안전한 절대 HTTPS 또는 /로 시작하는 루트 상대 URL만 허용. |
sections[].items | string[] 또는 null · 선택 | 생략/null/빈 배열 허용. 항목은 문자열만 가능하며 null/객체 불가. |
brandColor | string · 선택 | 생략 가능, null 불가. #RGB 또는 #RRGGBB 16진수만 허용. |
copy | object · 선택 | 생략/빈 객체 허용, null 불가. 자유로운 키의 문자열 사전이며 값에 null/숫자/객체 불가. 사전 키는 대소문자를 구분하고 동일 키 중복 불가. |
외부 UTF-8 요청 본문은 1 MiB (1,048,576바이트)까지이며 스트리밍에도 적용됩니다. 외부 본문과 contentJson 내부 JSON의 최대 깊이는 각각 64입니다. contentJson 100,000자는 외부 바이트 제한과 별개이며 이스케이프도 외부 크기에 포함됩니다. 모든 문자열과 필드명은 유효한 Unicode여야 합니다.
스키마 필드명은 대소문자를 구분하지 않아 PascalCase도 허용하지만 알 수 없는 필드와 중복 필드(title + Title 포함)는 외부·내부·섹션 어디서든 거부됩니다. copy의 사전 키만 대소문자를 구분합니다. 이미지 URL은 //로 시작하는 프로토콜 상대 주소, HTTP/data/javascript, 사용자 정보, 공백·제어문자·역슬래시를 허용하지 않습니다. URL 디코딩 후의 공백·제어문자·역슬래시 및 // 우회도 검사합니다. URL 검증은 이미지의 실제 존재나 콘텐츠 안전성을 보장하지 않습니다.
{
"sections": [
{ "type": "hero", "title": "테스트 베이커리", "subtitle": "승인된 테스트 사이트", "body": null, "cta": "방문 안내", "imageUrl": "/images/test.jpg", "items": null },
{ "type": "about", "title": "소개", "body": "매일 아침 빵을 굽습니다." },
{ "type": "services", "title": "메뉴", "items": ["크루아상", "식빵"] },
{ "type": "portfolio", "imageUrl": "https://images.example.invalid/test.jpg" },
{ "type": "contact", "title": "방문 안내", "body": "테스트 전용 안내입니다." }
],
"brandColor": "#C99550",
"logoUrl": null,
"copy": { "tagline": "테스트 전용", "Tagline": "서로 다른 사전 키" }
}
{
"title": "테스트 베이커리",
"contentJson": "{\n \"sections\": [\n { \"type\": \"hero\", \"title\": \"테스트 베이커리\", \"subtitle\": \"승인된 테스트 사이트\", \"body\": null, \"cta\": \"방문 안내\", \"imageUrl\": \"/images/test.jpg\", \"items\": null },\n { \"type\": \"about\", \"title\": \"소개\", \"body\": \"매일 아침 빵을 굽습니다.\" },\n { \"type\": \"services\", \"title\": \"메뉴\", \"items\": [\"크루아상\", \"식빵\"] },\n { \"type\": \"portfolio\", \"imageUrl\": \"https://images.example.invalid/test.jpg\" },\n { \"type\": \"contact\", \"title\": \"방문 안내\", \"body\": \"테스트 전용 안내입니다.\" }\n ],\n \"brandColor\": \"#C99550\",\n \"logoUrl\": null,\n \"copy\": { \"tagline\": \"테스트 전용\", \"Tagline\": \"서로 다른 사전 키\" }\n}"
}
# 서버 전용. munpae.kr 서비스 홈페이지에 PUT 테스트하지 마세요.
# home이 없으면 생성하여 마케팅 대체 화면까지 바꿉니다. 백업·승인 후 실행하세요.
# HTTP 금지. --location/-L 금지. 키를 로그에 남기는 --verbose/--trace 금지.
curl --proto '=https' --max-redirs 0 --fail-with-body --include \
--request PUT 'https://authorized-test-tenant.example.invalid/api/v1/landing-page' \
--header "X-Api-Key: ${MUNPAE_API_KEY:?Set a server-only test tenant key}" \
--header 'Content-Type: application/json' \
--data-raw '{
"title": "테스트 베이커리",
"contentJson": "{\n \"sections\": [\n { \"type\": \"hero\", \"title\": \"테스트 베이커리\", \"subtitle\": \"승인된 테스트 사이트\", \"body\": null, \"cta\": \"방문 안내\", \"imageUrl\": \"/images/test.jpg\", \"items\": null },\n { \"type\": \"about\", \"title\": \"소개\", \"body\": \"매일 아침 빵을 굽습니다.\" },\n { \"type\": \"services\", \"title\": \"메뉴\", \"items\": [\"크루아상\", \"식빵\"] },\n { \"type\": \"portfolio\", \"imageUrl\": \"https://images.example.invalid/test.jpg\" },\n { \"type\": \"contact\", \"title\": \"방문 안내\", \"body\": \"테스트 전용 안내입니다.\" }\n ],\n \"brandColor\": \"#C99550\",\n \"logoUrl\": null,\n \"copy\": { \"tagline\": \"테스트 전용\", \"Tagline\": \"서로 다른 사전 키\" }\n}"
}'
// munpae.kr에 실행 금지. 승인된 테스트 테넌트만 사용하며 기존 원문을 먼저 백업하세요.
const apiKey = process.env.MUNPAE_API_KEY;
if (!apiKey) throw new Error('Server-only test tenant key required');
const input = {
"title": "테스트 베이커리",
"contentJson": "{\n \"sections\": [\n { \"type\": \"hero\", \"title\": \"테스트 베이커리\", \"subtitle\": \"승인된 테스트 사이트\", \"body\": null, \"cta\": \"방문 안내\", \"imageUrl\": \"/images/test.jpg\", \"items\": null },\n { \"type\": \"about\", \"title\": \"소개\", \"body\": \"매일 아침 빵을 굽습니다.\" },\n { \"type\": \"services\", \"title\": \"메뉴\", \"items\": [\"크루아상\", \"식빵\"] },\n { \"type\": \"portfolio\", \"imageUrl\": \"https://images.example.invalid/test.jpg\" },\n { \"type\": \"contact\", \"title\": \"방문 안내\", \"body\": \"테스트 전용 안내입니다.\" }\n ],\n \"brandColor\": \"#C99550\",\n \"logoUrl\": null,\n \"copy\": { \"tagline\": \"테스트 전용\", \"Tagline\": \"서로 다른 사전 키\" }\n}"
};
// contentJson은 문자열입니다. 객체를 만들었다면 JSON.stringify(content)를 넣으세요.
const response = await fetch('https://authorized-test-tenant.example.invalid/api/v1/landing-page', {
method: 'PUT', redirect: 'error', credentials: 'omit',
headers: { 'X-Api-Key': apiKey, 'Content-Type': 'application/json' },
body: JSON.stringify(input)
});
const isJson = response.headers.get('content-type')?.includes('json');
const result = isJson ? await response.json() : null;
if (!response.ok) {
// 409 landing_page_busy: Retry-After: 1. 자동 재시도하지 말고 현재 내용을 확인하세요.
throw new Error(`PUT HTTP ${response.status}; Retry-After=${response.headers.get('retry-after') ?? '없음'}`);
}
const page = result; // 201 생성 / 200 교체. 이미 공개됨. 응답/키는 로깅하지 않습니다.
응답과 저장 의미 · 즉시 공개
GET 200 및 PUT 201/200 응답은 { id, slug, title, contentJson, createdAt, updatedAt }입니다. id는 UUID, slug는 home, title/contentJson은 문자열, createdAt/updatedAt은 ISO 8601 시각입니다. contentJson은 JSON 객체가 아닙니다. 응답에 tenantId나 draft 래퍼는 없습니다.
{
"id": "22222222-2222-4222-8222-222222222222",
"slug": "home",
"title": "테스트 베이커리",
"contentJson": "{\n \"sections\": [\n { \"type\": \"hero\", \"title\": \"테스트 베이커리\", \"subtitle\": \"승인된 테스트 사이트\", \"body\": null, \"cta\": \"방문 안내\", \"imageUrl\": \"/images/test.jpg\", \"items\": null },\n { \"type\": \"about\", \"title\": \"소개\", \"body\": \"매일 아침 빵을 굽습니다.\" },\n { \"type\": \"services\", \"title\": \"메뉴\", \"items\": [\"크루아상\", \"식빵\"] },\n { \"type\": \"portfolio\", \"imageUrl\": \"https://images.example.invalid/test.jpg\" },\n { \"type\": \"contact\", \"title\": \"방문 안내\", \"body\": \"테스트 전용 안내입니다.\" }\n ],\n \"brandColor\": \"#C99550\",\n \"logoUrl\": null,\n \"copy\": { \"tagline\": \"테스트 전용\", \"Tagline\": \"서로 다른 사전 키\" }\n}",
"createdAt": "2026-09-07T09:00:00+00:00",
"updatedAt": "2026-09-07T09:00:00+00:00"
}
- home 없음: PUT은 201 Created, 새 페이지 ID와 생성 시각,
Location: /api/v1/landing-page를 반환하고 새 콘텐츠 리비전을 만듭니다. 마케팅 대체 화면이 있었다면 이 페이지가 대신 공개됩니다. - home 있음: PUT은 200 OK. title/contentJson 전체 교체이며 PATCH·병합이 아닙니다. 생략한 선택 콘텐츠도 이전 값과 병합하지 않습니다. 교체 직전 contentJson을 리비전에 남기고 updatedAt을 갱신합니다.
- 기존 Page.Id와 createdAt은 보존합니다. 시안 발행으로 연결된 페이지도 ID를 바꾸지 않아 기존 시안 발행 연결을 유지합니다. 새 시안이나 테넌트를 생성하지 않습니다.
- 저장 즉시 공개됩니다. 별도 발행 단계가 없으며 AI 호출·AI 크레딧 차감·무료 생성 횟수 사용이 없습니다. GET과 키 인증 처리 이후 응답은
Cache-Control: no-store입니다. - 같은 테넌트의 동시 API 쓰기만 직렬화합니다. 관리자 웹 편집과의 상호 배제, ETag / If-Match / CAS를 지원하지 않습니다. 오래된 내용을 덮어쓸 수 있으므로 웹 편집과 동시에 저장하지 마세요. 응답을 잃으면 GET으로 먼저 확인하고 무조건 재전송하지 마세요.
랜딩페이지 오류 (무료 시안 오류와 별개)
| 상태 | 본문 / 의미 | 대응 |
|---|---|---|
| 401 | error 문자열. 키 누락·잘못된 키·다른 테넌트·만료·폐기·HTTP 요청 등. | HTTPS와 해당 호스트의 유효 키를 확인. 쿠키 로그인으로 해결되지 않으며 리다이렉트로 키를 전달하지 않습니다. 본문 검증보다 인증이 먼저입니다. |
| 404 | GET: 유효 키지만 home 없음. 또는 인증 전 호스트 미들웨어가 알 수 없는·미검증·비활성 호스트 거부. | 응답 JSON 형식 및 인증 성공을 단정하지 마세요. 미들웨어 404는 키 인증 응답의 no-store 보장 범위 밖입니다. |
| 415 | unsupported_media_type | PUT Content-Type을 application/json으로 설정. |
| 400 | Problem Details의 errors 사전 | 외부/내부 JSON, Unicode, 필드·타입·길이·깊이·URL·색상 검증 수정. 레거시 원문도 그대로 PUT하면 거부될 수 있습니다. |
| 413 | request_too_large | 외부 본문 1,048,576바이트 초과. contentJson 100,000자 초과는 400입니다. |
| 409 | landing_page_busy · Retry-After: 1 | 다른 랜딩 쓰기 진행 중. 1초 이후 현재 상태와 작업 충돌을 확인하고 명시적으로 재시도. |
코드 오류 415/413/409는 { "code": "…", "error": "…" }, 401은 { "error": "…" }입니다. 프록시/네트워크/예외 응답은 JSON이 아닐 수 있습니다.
API 키는 어디서 준비하나요?
고객사 관리자 계정으로 로그인하고 API 키 관리에서 발급하세요. 키는 발급 직후 한 번만 보이므로 서버의 비밀 저장소에 보관해 주세요.
API 키 발급·조회 확인·폐기 순서
발급·폐기 UI는 사용 가능합니다. 이제 랜딩페이지 GET으로 읽기 전용 키 확인을 할 수 있습니다. 무료 시안 API 호출 성공은 키 인증 성공을 증명하지 않습니다. 키 인증·만료·폐기·다른 고객사 차단은 자동화 테스트에서도 검증하며 별도 공개 테스트 호출 경로는 제공하지 않습니다.
- 해당 고객사의 검증된 활성 도메인에서 관리자 계정으로 로그인하여 API 키 관리를 엽니다. 편집자는 발급·폐기할 수 없습니다.
- “발급 확인용”처럼 이름을 정해 키 한 개를 발급합니다. 원문은 발급 응답에서 한 번만 표시됩니다. 화면을 떠난 뒤에는 다시 조회할 수 없습니다.
- 해당 테스트 호스트에서 GET으로 확인하세요. 200은 조회 성공, 404는 home 없음일 수도 있으므로 알려진 검증·활성 호스트인지 별도 확인합니다. 점검용 키를 같은 화면에서 폐기한 뒤 동일 호스트 GET이 401인지 확인합니다. PUT은 키 확인용으로 사용하지 마세요. 새로고침으로 발급 POST를 다시 제출하면 다른 키가 발급될 수 있습니다.
- 키는 서버 환경 변수/비밀 저장소에만 보관합니다. 90일 후 만료되며 유출·분실 시 폐기하고 재발급하세요. 브라우저 JavaScript, 프런트엔드, 모바일 앱 번들, 공개 저장소, AI 대화, 스크린샷에 원문을 넣지 마세요.
랜딩페이지 GET·PUT에는 HTTPS의 X-Api-Key 헤더를 사용합니다. 키는 발급한 고객사의 기본 보호 API 권한 범위이며 파일 API는 별도 발행 권한이 필요합니다. 키는 다른 고객사에서 사용할 수 없습니다. 웹 로그인 쿠키로 보호 API 인증을 대체할 수 없고, 키도 웹 관리자 로그인이나 무료 시안 소유자 쿠키를 대체하지 않습니다. 키 관리 화면은 no-store/no-referrer를 적용하고 분석 스크립트를 제외합니다.
블로그 소식도 API로 관리할 수 있나요?
네. 카페의 이번 주 메뉴처럼 글을 먼저 초안으로 저장하고, 확인한 뒤 발행할 수 있어요. 고객사 도메인과 같은 고객사의 X-Api-Key를 사용하세요. 웹 관리자에서 작성한 글도 같은 목록에 보여요.
publish: false는 비공개 초안, publish: true는 공개 발행이에요. 발행된 글에 false를 보내면 공개가 해제돼요. 무료 플랜은 초안을 포함해 글 3개까지 작성할 수 있으며 기존 글 수정은 한도를 추가 사용하지 않아요.
{
"title": "이번 주 카페 메뉴를 소개해요",
"slug": "weekly-menu",
"summary": "이번 주에 준비한 메뉴와 방문 안내예요.",
"contentMarkdown": "## 이번 주 메뉴\n\n따뜻한 라떼와 갓 구운 스콘을 준비했어요.",
"category": "매장 소식",
"tags": "카페,메뉴",
"coverImageUrl": null,
"ctaType": "inquiry",
"ctaReservationUrl": null,
"publish": false
}
블로그 API 경로, 입력 규칙과 실행 예시
| 요청 | 결과 |
|---|---|
GET /api/v1/blog-posts | 목록. page 기본 1, pageSize 기본 20·최대 100. status=draft 또는 published로 필터링할 수 있어요. |
GET /api/v1/blog-posts/{id} | 해당 고객사의 글 한 개. 본문과 모든 편집 필드를 반환해요. |
POST /api/v1/blog-posts | 글 생성. 201과 Location에 새 글의 API 주소를 반환해요. |
PUT /api/v1/blog-posts/{id} | 전체 수정·발행 전환. 성공하면 200을 반환해요. 생략한 선택 필드는 기본값으로 바뀌므로 필요한 내용을 모두 보내세요. |
목록은 { page, pageSize, total, items } 형식이에요. 최신 생성일·ID 순서로 정렬하며 목록에는 큰 마크다운 본문을 포함하지 않아요. page는 1~1000000 범위예요.
목록과 상세에는 id, slug, title, summary, status, createdAt, updatedAt, publishedAt, publicUrl이 있어요. status는 draft 또는 published이며 공개 URL과 발행 시각은 초안일 때 null이에요. 상세·생성·수정 응답에는 본문과 나머지 편집 필드도 포함돼요.
필수 입력은 title(1~200자)과 publish(true/false)예요. 선택 필드는 slug(120자), summary(500자), contentMarkdown(100,000자), category(60자), tags(쉼표 구분·200자), coverImageUrl(2,048자), ctaType, ctaReservationUrl(500자)이에요. 제목·요약은 앞뒤 공백을 제거하고 본문은 원문을 보관해요.
ctaType은 inherit(기본 설정), inquiry(문의), phone(전화), reservation(예약), hidden(숨김) 중 하나예요. 생략하면 inherit이며 reservation에는 HTTPS 예약 링크가 필요해요. 대표 이미지는 HTTPS 또는 /로 시작하는 안전한 주소를 사용하세요. 업로드는 웹 관리자에서 진행해요.
slug는 기존 CMS와 같이 정리되며, 중복되면 -2, -3을 붙여 저장해요. 응답의 최종 slug와 publicUrl을 사용하세요. 태그는 대소문자 중복을 제거하고 최대 10개로 정리돼요. 발행 글 수정은 최초 발행 시각을 유지하고, 비공개 전환 후 다시 발행하면 새 발행 시각이 기록돼요.
본문은 기존 블로그의 마크다운 렌더러로 표시하며 원시 HTML을 실행하지 않아요. API가 AI 글을 생성하거나 상담을 접수하지는 않아요. 고객사 블로그는 문패 서비스 블로그용 Slack 채널에 알리지 않으며, 문패 운영 블로그만 새 발행·재발행 시 알립니다. 이미 발행된 글의 수정에는 다시 알리지 않아요.
# 고객사 테스트 주소로 바꾸세요. 키는 서버 환경 변수에서 읽어요.
curl --proto '=https' --max-redirs 0 --fail-with-body \
'https://authorized-test-tenant.example.invalid/api/v1/blog-posts?page=1&pageSize=20&status=draft' \
--header "X-Api-Key: ${MUNPAE_API_KEY:?Set a server-only test tenant key}"
const apiKey = process.env.MUNPAE_API_KEY;
if (!apiKey) throw new Error('Set MUNPAE_API_KEY on the server');
const base = 'https://authorized-test-tenant.example.invalid';
const input = {
"title": "이번 주 카페 메뉴를 소개해요",
"slug": "weekly-menu",
"summary": "이번 주에 준비한 메뉴와 방문 안내예요.",
"contentMarkdown": "## 이번 주 메뉴\n\n따뜻한 라떼와 갓 구운 스콘을 준비했어요.",
"category": "매장 소식",
"tags": "카페,메뉴",
"coverImageUrl": null,
"ctaType": "inquiry",
"ctaReservationUrl": null,
"publish": false
};
async function call(path, method = 'GET', body) {
const response = await fetch(base + path, {
method, redirect: 'error', credentials: 'omit',
headers: { 'X-Api-Key': apiKey, 'Content-Type': 'application/json' },
...(body === undefined ? {} : { body: JSON.stringify(body) })
});
if (!response.ok) throw new Error(`Blog API HTTP ${response.status}`);
return response.json();
}
// POST는 새 글을 만들어요. 응답을 잃으면 목록을 먼저 확인하고 자동 재시도하지 마세요.
const created = await call('/api/v1/blog-posts', 'POST', input);
const path = '/api/v1/blog-posts/' + created.id;
const current = await call(path);
// PUT에는 전체 입력을 보내요. 응답 객체 전체를 그대로 보내면 안 돼요.
// 발행할 때만 내용을 검토한 뒤 publish를 true로 바꾸세요.
const saved = await call(path, 'PUT', { ...input, slug: current.slug, title: '이번 주 메뉴 안내', publish: false });
// API 키와 비공개 글 내용을 로그에 남기지 마세요.
오류: 400은 JSON·입력·페이지네이션 검증, 401은 키 인증, 403 blog_limit_reached는 플랜 한도, 404 blog_not_found는 글 없음 또는 다른 고객사의 글이에요. 잘못된 호스트도 앞 단계에서 404일 수 있어요. 409 blog_busy는 저장 중이며 Retry-After: 1을 반환해요. 드물게 웹 편집과 slug가 충돌하면 409 blog_slug_conflict를 반환해요. 413은 요청 본문 1 MiB 초과, 415는 JSON Content-Type 누락이에요.
입력 오류는 errors 사전, 나머지 블로그 코드 오류는 code/error를 확인하세요. 인증 응답은 error만 반환할 수 있어요. 인증을 본문 파싱보다 먼저 수행하고 응답은 no-store로 처리해요. JSON 최대 깊이는 32이며 중복·알 수 없는 필드·잘못된 Unicode는 거절해요.
같은 고객사의 API 저장은 함께 실행되지 않도록 잠그지만, 기존 웹 편집은 같은 잠금을 사용하지 않아요. 동시 편집이나 응답 유실 시 먼저 현재 글을 조회하세요. POST 재시도는 새 글을 만들 수 있어요. 부분 수정, 삭제, 예약 발행, 이미지 업로드와 AI 글 생성 API는 제공하지 않아요.
우리 사이트의 고객 문의도 확인할 수 있나요?
네. 문의 폼과 API로 받은 일반 고객 문의는 같은 문의 관리에 모여요. 목록에서 접수 번호를 찾고, 필요한 문의만 상세 조회하세요.
홈페이지 제작 신규 상담과 수정·기술문의는 별도 업무예요. 이 API가 문패 신규 상담이나 기술문의에 데이터를 추가하거나 Slack에 고객 정보를 보내지는 않아요.
고객 문의 API 경로와 서버 연동 예시
| 요청 | 하는 일 |
|---|---|
GET /api/v1/inquiries | page 기본 1, pageSize 기본 20·최대 100. isRead=true 또는 false로 필터링해요. |
GET /api/v1/inquiries/{id} | 이름·이메일·문의 내용까지 조회해요. 조회만으로 읽음 상태가 바뀌지 않아요. |
POST /api/v1/inquiries | 새 문의 접수. 성공하면 201과 Location을 반환해요. |
PATCH /api/v1/inquiries/{id} | { "isRead": true } 또는 false로 읽음 여부만 바꿔요. 성공하면 본문 없는 204예요. |
DELETE /api/v1/inquiries/{id} | 문의와 개인정보를 삭제해요. 성공하면 본문 없는 204, 이미 삭제됐거나 없는 문의는 404예요. API 복구 기능은 없어요. |
목록 응답은 page, pageSize, total, items이며 각 항목에는 id, isRead, createdAt만 있어요. 상세 응답은 여기에 name, email, message를 더해요. 생성 응답도 개인정보를 반복하지 않고 접수 번호·읽음 여부·접수 시각만 반환해요.
읽음은 회신 완료가 아니에요. 현재 회신 여부·회신 시각·답변 발송은 관리하지 않아요. 답변은 기존 업무 도구에서 보내세요. 접수 시각은 서버가 기록하며 신규 문의는 읽지 않음으로 시작해요.
등록에는 name(100자), email(254자·이메일 형식), message(10,000자), privacyConsent(true)가 필수예요. 연락처는 phone(선택·최대 30자)으로 보내며 문의 상세와 CSV·엑셀에도 포함돼요. 앞뒤 공백은 제거해요. 신청자에게 수집 항목·문의 접수 및 회신 목적·보유와 삭제 방법을 안내하고 실제 동의를 받은 뒤 호출하세요. 연동 서버가 동의 증빙을 보관해야 하며 이 API는 동의 이력을 별도 저장하지 않아요.
{
"name": "예시 고객",
"email": "customer@example.invalid",
"phone": "010-0000-0000",
"message": "방문 예약 방법을 안내받고 싶습니다.",
"privacyConsent": true
}
curl --proto '=https' --max-redirs 0 --fail-with-body \
'https://authorized-test-tenant.example.invalid/api/v1/inquiries?page=1&pageSize=20&isRead=false' \
--header "X-Api-Key: ${MUNPAE_API_KEY:?Set a server-only test tenant key}"
const apiKey = process.env.MUNPAE_API_KEY;
if (!apiKey) throw new Error('Set MUNPAE_API_KEY on the server');
const base = 'https://authorized-test-tenant.example.invalid';
async function call(path, method = 'GET', body) {
const response = await fetch(base + path, {
method, redirect: 'error', credentials: 'omit', cache: 'no-store',
headers: { 'X-Api-Key': apiKey, 'Content-Type': 'application/json' },
...(body === undefined ? {} : { body: JSON.stringify(body) })
});
if (!response.ok) throw new Error(`Inquiry API HTTP ${response.status}`);
return response.status === 204 ? undefined : response.json();
}
const list = await call('/api/v1/inquiries?isRead=false&page=1&pageSize=20');
// list.items에는 id, isRead, createdAt만 있어요. 다음 목록은 page를 늘려 읽으세요.
const first = list.items[0];
if (first) {
const path = '/api/v1/inquiries/' + first.id;
const detail = await call(path);
// detail의 이름·이메일·본문은 권한 있는 업무 화면에서만 사용하세요.
// 실제 담당자가 확인한 뒤 실행하세요. 읽음은 답변 발송을 뜻하지 않아요.
// await call(path, 'PATCH', { isRead: true });
// 보유 목적이 끝나 삭제하기로 결정한 경우에만 실행하세요. 복구할 수 없어요.
// await call(path, 'DELETE');
}
// 응답, API 키, 고객 개인정보를 콘솔·Slack·오류 추적 도구에 남기지 마세요.
자기 고객사의 HTTPS 주소와 서버 전용 X-Api-Key만 사용하세요. 브라우저 JavaScript에는 키를 넣지 마세요. 응답은 no-store이며 목록·상세·오류 본문을 로그나 Slack, GitHub에 복제하지 마세요.
이름·이메일·본문은 삭제할 때까지 저장돼요. 자동 만료는 없으므로 운영자가 보유 목적이 끝난 문의를 관리자 화면이나 DELETE로 삭제해야 해요. 삭제는 운영 DB에 적용되며 외부로 내보낸 사본과 백업은 각 저장소의 보유 정책에 따라 별도로 관리하세요.
JSON은 64 KiB·깊이 8까지이며 필드 이름은 위 예시의 대소문자를 구분해요. 중복·알 수 없는 필드와 잘못된 Unicode는 거절해요. 잘못된 입력·동의·페이지 값은 400, 키 오류는 401, 없거나 다른 고객사의 문의는 404, 큰 본문은 413, JSON이 아니면 415예요.
POST 중복 방지 키는 아직 없어요. 응답이 유실되면 자동 재전송하지 말고 접수 여부를 먼저 확인하세요. PATCH·DELETE는 개인정보 본문을 보내지 않으며, 다른 작업과 동시에 처리한 경우 가장 나중에 반영된 상태를 확인하세요.
새 문의를 자동화 도구로 바로 받을 수 있나요?
네. 고객사 관리자가 외부 웹훅에 내 수신 서버를 등록하면 고객 문의 접수·블로그 발행·홈페이지 저장 소식을 받아볼 수 있어요. 등록은 고객사 관리자 로그인으로 진행하며, API 키만으로 연결 설정을 바꾸지는 못해요.
기본 웹훅에는 이름·연락처·내용을 넣지 않아요. 이벤트 종류와 대상 ID를 받아 필요한 업무를 시작하세요. 결제·구독 변경, 신규 제작 상담과 기술문의는 포함하지 않아요.
ERP/CRM에 문의 내용을 연결하려면
연결을 등록하지 않으면 외부 전달 없이 문패에 문의를 저장해요. 웹 문의와 POST /api/v1/inquiries는 같은 경로로 저장하고 inquiry.created를 만들어요. AI 시안 요청과 신규 제작 상담은 이 연동에 포함하지 않아요.
문의 내용을 ERP/CRM으로 옮기려면 고객사별 수신 서버에서 문의 상세 조회 옵션을 켜세요. MUNPAE_INQUIRY_API_ORIGIN에 해당 고객사 HTTPS 주소, MUNPAE_TENANT_API_KEY에 서버용 API 키, MUNPAE_FORWARD_URL에 ERP/CRM 수신 주소를 설정해요. 서명·고객사 검증 후 GET /api/v1/inquiries/{id}로 이름·연락처·내용을 조회해서 전달합니다. 이 옵션을 설정하지 않으면 기존처럼 ID만 전달해요.
수신 서버 예제는 문의 외 이벤트를 건너뛰고, 중복 이벤트를 영구 대기열에서 확인해요. ERP/CRM에서는 Idempotency-Key 또는 고객사 ID와 문의 ID를 고유 키로 사용해 같은 문의를 두 번 만들지 않도록 구성하세요. 문의 API에 동일한 POST를 다시 보내면 새 문의가 되므로 접수 요청의 재전송도 호출 측에서 관리해야 해요.
문패의 전달 성공은 수신 서버 응답 성공을 뜻해요. ERP/CRM 저장 완료는 수신 서버의 처리 이력에서 확인하세요. 실패해도 원본 문의는 문패에 남으며, 전달 이력에서 원본을 찾을 수 있어요. 설정·재시도·장애 확인 안내
Slack으로 받으려면 Slack 앱의 Incoming Webhook URL을 등록하세요. hooks.slack.com/services/… 주소를 자동 인식해 읽기 쉬운 메시지로 보내요. 별도 서명키 설정 없이 테스트 ping으로 채널 수신을 확인할 수 있어요. Slack의 일반 메시지 수신은 문패 서명·중복 검증을 하지 않으므로 재시도 때 같은 메시지가 도착할 수 있어요.
이벤트, 서명과 Make·Zapier 연결 방법
고객사당 5개까지 등록할 수 있어요. 공인 DNS 호스트의 HTTPS 443 주소만 지원하고 IP 주소·로그인 정보·쿼리·프래그먼트는 허용하지 않아요. 수신 URL은 호스트만 다시 표시하며 일반 웹훅의 서명키는 등록 직후 한 번만 보여줘요.
inquiry.created는 웹 폼·API 문의 접수, blog.published는 최초 발행·재발행, landing_page.updated는 홈페이지 API 저장·관리자 편집 때 발생해요. webhook.ping은 테스트 버튼으로 연결별 1분에 한 번 보낼 수 있어요. 모든 연결은 이 이벤트를 함께 받아요. 일반 웹훅 수신 서버는 type으로 분기하고 Slack은 이벤트별 안내 메시지를 받아요.
{
"id": "11111111-1111-4111-8111-111111111111",
"version": 1,
"type": "inquiry.created",
"tenantId": "22222222-2222-4222-8222-222222222222",
"occurredAt": "2026-09-07T08:00:00+00:00",
"data": { "id": "33333333-3333-4333-8333-333333333333" }
}
최상위 id는 중복 방지용 이벤트 ID이고, data.id는 조회할 문의·글·페이지 ID예요. ping의 data.id는 연결 ID예요. 상세 정보는 서버에서 해당 고객사의 API 키로 따로 조회하세요.
webhook-id: 11111111-1111-4111-8111-111111111111
webhook-timestamp: <이번 전송 시도의 Unix 초>
webhook-signature: v1,<Base64 HMAC-SHA256>
서명 원문 = id + "." + timestamp + "." + 파싱 전 JSON 원문
HMAC 키 = whsec_ 접두사를 제거하고 Base64 디코딩한 32바이트
서명만 확인하면 끝이 아니에요. 파싱 전 본문으로 HMAC을 비교하고, 현재 시각과 5분 이내인지 확인하세요. 기대한 tenantId·버전·종류를 검사한 뒤 영속 저장소에서 이벤트 ID를 한 번만 접수해야 해요. JSON을 다시 직렬화해 서명을 계산하거나 메모리만으로 중복을 막지 마세요.
// 문서에 연결된 customer-webhook-receiver.mjs를 함께 배치하세요.
import { verifyWebhook, acceptOnce } from './customer-webhook-receiver.mjs';
// HTTP 핸들러에서 받은 headers, rawBody(Buffer)를 전달하세요.
export async function accept(headers, rawBody) {
const event = verifyWebhook({
headers, rawBody,
secret: process.env.MUNPAE_WEBHOOK_SECRET,
tenantId: process.env.MUNPAE_TENANT_ID
});
// 서명·5분 시각·테넌트·스키마를 확인한 뒤, 영속 디스크에 ID를 한 번만 저장해요.
await acceptOnce(process.env.MUNPAE_WEBHOOK_SPOOL, event, rawBody);
// 이 작업이 성공한 뒤에만 HTTP 202로 응답하세요.
// Make/Zapier 전달은 별도 소비자가 처리하고 최상위 event.id로 중복 업무를 막으세요.
}
- Make의 Custom webhook 또는 Zapier의 Catch Hook/Catch Raw Hook으로 수신 URL을 만드세요.
- Node.js 수신 예제를 HTTPS 서버와 영속 비공개 디스크에 배치하고, 문패에는 이 검증 서버의 주소를 등록하세요.
- 예제 서버의 서명키·고객사 ID·스풀 경로를 환경 변수로 설정하세요. MUNPAE_FORWARD_URL에는 Make/Zapier 주소를 넣으면 검증한 이벤트를 뒤에서 전달해요. 예제는 단일 프로세스용이며 외부 HTTPS 프록시가 필요해요.
- ping을 보내 양쪽 이력을 확인하고 type으로 업무를 나누세요. 수신 서버의 재전송도 생길 수 있으므로 Make/Zapier에서도 최상위 id로 중복 작업을 막으세요.
- Notion에 기록하려면 Make/Zapier의 Notion 연결 권한을 별도로 설정해 최소 필드만 매핑하세요. Notion API 주소 자체는 문패 웹훅 수신 주소가 아니에요.
직접 Make/Zapier URL로 ping이 도착한 것만으로 서명 검증이 완료되지는 않아요. 업무 처리 전에 원문 서명·시각·중복을 검증할 수 없다면 예제의 검증 서버를 사용하세요. 스풀은 미전달 작업을 보존하고 전달 완료 기록도 최소 7일 이상 유지하세요.
수신 서버는 검증한 이벤트를 영속 대기열에 넣고 빠르게 2xx로 응답하세요. 문패는 총 10초 안에 응답을 기다리며 네트워크 오류·408·429·5xx에 최대 5회 전송해요. 대기는 1분·5분·30분·2시간, 최초 접수 후 24시간이 지나면 종료해요. 리다이렉트와 다른 4xx는 재시도하지 않아요. 순서는 보장하지 않으며 ID와 본문은 재시도에도 같고 서명 시각은 갱신돼요.
중지하면 새 이벤트와 대기 이벤트를 멈추며 다시 사용해도 과거 취소분을 보내지 않아요. 삭제하면 URL·키·이름을 제거해요. 이미 전송 중인 요청은 끝날 수 있고 개인정보 없는 이력은 30일 뒤 정리해요. 응답 본문은 저장하지 않아요. 키 분실·유출 시 연결을 삭제하고 다시 등록하세요.
파일로 만든 홈페이지를 발행하세요
고객은 자기 사이트를, 컨설턴트는 연결된 고객 사이트를 같은 파일 API로 작업합니다. 키와 요청 주소를 먼저 확인하세요. 기존 블로그와 문의 데이터는 유지됩니다.
파일 준비와 필수 연결
발행된 홈에는 해당 고객의 블로그·문의 메뉴가 항상 제공됩니다. 사업장명·블로그·문의가 문패 상단바에 통합되며 제작 파일에 버튼이 빠져도 사용할 수 있습니다. 기본 템플릿은 별도 상단 메뉴를 만들지 않습니다. 문의는 새 창 대신 현재 화면의 대화상자로 열립니다.
제작 템플릿과 Python 업로드 도구로 시작하세요. 템플릿의 문패 메시지 연결을 유지하면 상담 버튼은 부모 화면의 문의 폼을 열고 블로그로 이동할 수 있습니다. 발행한 홈에서 직접 확인하세요. 미리보기 파일만 열면 부모 연결은 동작하지 않습니다.
index.html필수. UTF-8 HTML·CSS·일반 JS와 PNG/JPEG/WebP/GIF를 상대 경로로 연결합니다.- 버전당 원본 합계 2 MiB·40개 파일, JSON 요청 3 MiB. 사이트당 10개 버전·원본 합계 20 MiB입니다.
- ZIP·SVG·영상 파일·외부 CDN·외부 fetch·ES 모듈·서버 코드는 지원하지 않습니다. YouTube는 템플릿의 메시지 연결을 사용합니다.
- 파일은 샌드박스에서 실행됩니다. 부모 문서·쿠키·localStorage 접근과 직접 폼 전송이 제한됩니다. API 키를 파일에 넣지 마세요.
- 파일 홈은 프레임으로 표시됩니다. JSON 홈과 동일한 검색 수집·SEO 결과를 보장하지 않습니다.
| 메서드·경로 (고객 사이트 호스트) | 작업 |
|---|---|
| GET /api/v1/site | 공개 버전·현재 revision 확인 |
| GET /api/v1/site/versions | 버전 목록 |
| POST /api/v1/site/versions | 파일 업로드 |
| POST /api/v1/site/versions/{id}/preview | 15분 미리보기 URL 발급 |
| POST /api/v1/site/publish | 발행·이전 버전 복원·JSON 모드 복구 |
| DELETE /api/v1/site/versions/{id} | 미사용 버전 삭제 |
| GET/POST /api/v1/site/blog-posts | 블로그 목록·작성 |
| GET/PUT /api/v1/site/blog-posts/{id} | 글 상세·수정·발행취소 |
업로드 → 미리보기 → 발행 → 복원
아래 JSON은 요청 형식을 확인할 최소 예제입니다. 실제 홈페이지는 위 템플릿에 블로그·문의 연결과 콘텐츠를 준비하세요. 예제 UUID는 새 작업마다 새 UUID로 교체하고 같은 요청의 재시도에는 그대로 사용합니다.
고객 사이트 주소의 POST /api/v1/site/versions에 JSON을 보냅니다. 응답의 versionId와 contentHash를 보관합니다.
{"requestId":"da7663a1-8726-472f-8aa3-59f336f5c410","files":[{"path":"index.html","base64":"PCFkb2N0eXBlIGh0bWw+PGh0bWwgbGFuZz0ia28iPjx0aXRsZT5EZW1vPC90aXRsZT48Ym9keT5EZW1vPC9ib2R5PjwvaHRtbD4="}]}POST /api/v1/site/versions/{id}/preview에 {}를 보내면 url과 expiresInSeconds: 900을 받습니다. 이 URL은 임시 접근 권한이므로 공개하지 마세요.
GET /api/v1/site로 현재 revision을 읽은 뒤 다음 JSON의 두 값을 응답 값으로 바꿔 POST /api/v1/site/publish로 보냅니다. 성공하면 새 revision과 공개 url을 반환합니다.
{"requestId":"b3d8e78b-5325-4d71-85ec-f7bbcbf31286","expectedRevision":"현재 revision","versionId":"업로드 응답의 versionId","confirmModeChange":true}첫 파일 발행은 JSON 편집 모드를 전환하며 홈페이지가 즉시 공개됩니다. JSON 원본은 삭제하지 않습니다. 수정은 새 버전을 올려 같은 주소에 발행합니다. 이전 버전 ID를 지정하면 복원되고, versionId: null이면 JSON 모드로 돌아갑니다. 복원도 최신 revision과 새 requestId로 요청하세요.
블로그 본문은 고객 블로그 API의 입력·발행 계약을 재사용합니다. 컨설턴트 키는 /api/v1/site/blog-posts 경로를 사용하세요. 고객 문의는 해당 고객 관리자 화면에서 확인하거나 고객 키로 문의 API를 사용합니다. 컨설턴트 키가 일반 문의·웹훅 API 권한을 갖는 것은 아닙니다.
파일 발행 오류와 재시도
401은 키·권한·만료·연결 해제, 404는 호스트·대상 존재 여부를 확인하세요. 400은 JSON·파일 유효성, 413은 요청 크기, 415는 JSON Content-Type을 확인합니다. 오류별 본문은 다르며 항상 JSON이라고 가정하지 마세요.
409 stale_revision은 다른 수정이 먼저 저장됐다는 뜻입니다. 최신 상태를 읽고 변경 내용을 확인한 뒤 새 requestId로 요청하세요. 동일 requestId에 다른 내용을 보내지 마세요. 응답을 못 받은 업로드·발행은 같은 ID와 내용으로 재시도합니다. 발행 중인 버전은 삭제할 수 없습니다. 429는 요청 제한이므로 즉시 반복 요청하지 마세요.
컨설턴트가 고객 사이트를 만드는 순서
- 플랫폼 운영자가 컨설턴트 관리에서 등록하고 전용 키를 발급합니다.
- 고객의 제작·공개·연락 정보 등록 동의를 확인하고
https://munpae.kr/api/consultant/v1/sites에 POST합니다. 생성 시 기본 페이지가 공개됩니다.
{"requestId":"df7c3e06-b40e-4bf1-b5b0-f813dac5d3c2","name":"예제 가게","subdomain":"example","customerEmail":"customer@example.test","customerConsent":true}응답의 siteId와 url을 보관하세요. 주소는 https://example-042.munpae.kr처럼 세 자리 난수가 붙습니다. 요청 이후에는 응답의 실제 URL을 사용합니다. 같은 요청 ID·본문은 같은 사이트를 반환하며 다른 본문은 409입니다. 신규 사이트는 컨설턴트당 100개까지입니다. 같은 경로에 GET하면 연결된 사이트 목록을 조회합니다.
기본 동작은 이메일 필수·숫자 3자리 접미사 주소이며 기존 요청의 재시도도 유지됩니다. 정확한 주소가 필요하면 exactSubdomain: true를 추가하세요. 소문자 주소가 이미 테넌트 또는 도메인에 등록돼 있으면 409 subdomain_unavailable이며 임의 주소로 대체하지 않습니다. 예약어와 기존 주소 형식 제한은 같습니다.
고객이 관리자 이메일을 나중에 연결하기로 동의한 경우에만 deferCustomerEmail: true와 customerEmail: null을 함께 지정할 수 있습니다. 이메일을 함께 제공하는 모순된 입력은 400입니다. 연락 이메일은 비워 두고 별도 감사 이력을 남기며 고객 계정·관리자 권한은 만들지 않습니다. 이후 확인된 이메일로 운영자가 인계하세요.
{"requestId":"3a0c9fdc-832b-4cb1-9f08-c3d3a7f1f172","name":"예제 학원","subdomain":"exampleacademy","customerEmail":null,"customerConsent":true,"exactSubdomain":true,"deferCustomerEmail":true}직접 생성한 고객의 별도 계약
고객사 이름 변경은 본사 호스트의 PATCH /api/consultant/v1/sites/{siteId}에 {"name":"새 고객사 이름","customerConsent":true}를 보냅니다. 직접 생성한 활성 연결 사이트에만 허용되며 이름은 공백이 아닌 1~100자(제어 문자 제외)입니다. 호스트·계약·기존 생성 요청 식별자는 바꾸지 않고 고객사 공통 이름과 감사 이력만 저장합니다. 상단바·블로그 등에서 같은 이름을 사용합니다.
본사 호스트의 GET/PUT /api/consultant/v1/sites/{siteId}/contract는 현재 컨설턴트가 직접 생성했고 연결이 활성인 사이트에만 허용됩니다. 단순 콘텐츠 위임 연결·다른 컨설턴트·본사 테넌트는 404입니다. 기존 키 인증·HTTPS·요청 제한을 그대로 적용합니다. GET은 siteId와 contract를 반환하며 아직 계약이 없으면 contract: null입니다.
{"monthlyAmount":180000,"serviceActive":true,"blogPostLimit":10,"customerConsent":true,"vatMode":"Unspecified","confirmed":false,"notes":"고객별 별도 계약. 관리자 이메일은 확인 후 연결."}금액(1~100000000원)·이용 상태·블로그 총량(0~1000000개)·고객 계약 동의는 필수이며 항상 별도 계약 플랜으로 저장합니다. 초안도 블로그 총량에 포함됩니다. 부가세가 미확정이면 Unspecified와 confirmed: false를 유지하세요. Exclusive(별도) 또는 Inclusive(포함)를 확인하기 전에는 계약을 확정할 수 없습니다. 메모는 최대 1000자이며 생략하면 지워집니다. PUT은 전달한 계약 상태를 대체하며 생략한 부가세·확정 값은 위 기본값으로 돌아갑니다.
커스텀 도메인·검색 등록·광고·CRM·AI 한도는 이 API로 변경할 수 없으며 운영자 설정을 보존합니다. 신규 계약의 AI 한도는 0입니다. 구독 플랜을 유료로 승급하거나 결제·청구·세금계산서 발행을 실행하지 않습니다. 운영자 계약 화면과 같은 잠금·검증·금융 이력을 사용하며, 기존 계약 유무와 무관하게 진행 중인 구독 주문이나 활성·등록 중·검토 중 정기결제가 있으면 409입니다. 고객 동의가 없거나 금액·부가세 등이 잘못되면 400입니다. 응답 유실 시 GET으로 상태를 확인하고 동일 상태를 PUT할 수 있으며 각 저장은 감사 이력을 남깁니다.
- 반환된 고객 사이트 호스트에 컨설턴트 키로 업로드·미리보기·발행을 진행합니다. 생성 API의 본사 호스트와 구분하세요.
- 발행된 홈페이지에서 블로그 작성·발행과 문의 신청을 확인합니다. 새 버전 수정과 이전 버전 복원도 확인하세요.
- 고객 가입 후 플랫폼 운영자가 확인된 이메일로 관리자 권한을 부여하거나 고객 관리자 구성원 초대로 인계합니다. 고객은 파일 발행 권한을 체크한 자신의 새 키로 작업할 수 있습니다.
- 고객은 파일 발행 메뉴에서 컨설턴트 접근을 해제할 수 있습니다. 키 폐기·컨설턴트 중지는 플랫폼에서 처리합니다. 이후 새 요청은 차단되지만 이미 실행 중인 요청은 취소되지 않습니다.
기존 고객 사이트 연결은 고객 위임을 확인한 플랫폼 운영자 작업입니다. 관리자 인계·연결 관리가 모두 공개 API로 제공되는 것은 아닙니다.
상단바 전화 상담만 표시
학원 단일 문의폼을 사용하는 파일 사이트는 <meta name="munpae:header-contact" content="phone">을 추가해 상단바의 예약 상담 버튼을 숨기고 전화 상담 링크만 표시할 수 있습니다. 유효한 munpae:contact-phone이 있을 때만 적용되며 본문의 온라인 상담·블로그 문의·/#contact 단일 문의폼은 유지됩니다. 다른 사이트와 기본 동작은 바뀌지 않습니다.
상담 채널 선택 팝업
파일 사이트의 index.html에 다음 메타데이터를 지정하면 상단 문의 버튼, munpae:inquiry 메시지, /#contact가 신청폼 대신 전화·카카오톡·네이버 톡톡 선택 팝업을 엽니다. 전화번호와 두 채팅 주소가 모두 유효할 때 적용됩니다.
<meta name="munpae:contact-mode" content="channels">
<meta name="munpae:contact-phone" content="1877-5253">
<meta name="munpae:contact-kakao" content="https://open.kakao.com/o/CHAT_ID">
<meta name="munpae:contact-naver" content="https://talk.naver.com/ct/CHAT_ID#nafullscreen">
채팅 링크는 위 호스트의 HTTPS 주소만 허용하며 새 창으로 엽니다. 설정하지 않은 사이트의 기존 상담 방식은 유지됩니다.
4. 오류가 나면 이 순서로 확인하세요
400이면 입력과 동의를, 401이면 해당 고객사의 API 키를 확인하세요. 429는 생성 한도 또는 잠시 요청이 몰린 경우예요. 응답의 오류 코드로 구분할 수 있어요.
같은 내용을 반복 전송하기 전에 응답을 확인하세요. 시안 API는 같은 중복 방지 키로 재시도하고, 랜딩페이지 PUT은 먼저 GET으로 현재 저장된 내용을 확인해 주세요.
무료 시안 API 오류 코드와 대응 방법
필드 검증 오류는 Problem Details의 errors 사전, 그 외 아래 코드 오류는 { "code": "…", "error": "…" }입니다. HTTP 상태와 Content-Type을 먼저 확인하세요.
| 상태 | 코드 / 본문 | 처리 |
|---|---|---|
| 400 | errors | 필수/동의/길이/전화번호 검증 실패. 필드별 오류를 수정합니다. |
| 400 | invalid_json | 잘못된 JSON, null 본문, 잘못된 타입, 알 수 없는 필드. |
| 400 | invalid_idempotency_key | Idempotency-Key에 비어 있지 않은 UUID 한 개를 보내세요. |
| 409 | idempotency_conflict | 동일 키로 보낸 요청 내용이 다르거나 기존 상담이 비식별 처리됐습니다. 자동 재시도하지 마세요. |
| 401 | invalid_identity | 인증되었으나 사용자 식별자가 없는 웹 로그인 정보. 다시 로그인하세요. API 키 누락을 뜻하지 않습니다. |
| 404 | 호스트/미리보기 접근 | 알 수 없는·미검증·비활성 호스트, 없거나 소유자가 아닌 시안. JSON 코드 형식을 보장하지 않습니다. |
| 413 | request_too_large | 본문이 16,384바이트 초과. |
| 415 | unsupported_media_type | JSON Content-Type으로 변경하세요. |
| 429 | free_quota_exceeded | 현재 3시간 창의 무료 3회 소진. quota.resetsAt과 Retry-After(남은 초)를 확인한 뒤 다시 요청하세요. |
| 429 | generation_in_progress | 동일 소유자 생성 진행 중. Retry-After: 5. 기존 요청 결과를 먼저 확인합니다. |
| 429 | rate_limited | 프로세스별 연결 IP 요청 제한. Retry-After: 60. 같은 버킷 사용자에게도 영향을 줄 수 있습니다. |
| 503 | generation_failed | AI 생성 실패. 잠시 후 확인하여 재시도하며 공급자 내부 정보는 반환하지 않습니다. |
{
"title": "One or more validation errors occurred.",
"status": 400,
"errors": { "name": ["이름을 입력해 주세요."] }
}
{
"code": "free_quota_exceeded",
"error": "이번 3시간의 무료 생성 횟수(3회)를 모두 사용했습니다. 초기화 시각 이후 다시 시도해 주세요.",
"quota": { "limit": 3, "used": 3, "remaining": 0, "resetsAt": "2026-09-07T12:00:00+00:00", "retryAfterSeconds": 10800 }
}
위 표는 애플리케이션이 처리하는 오류입니다. 프록시/네트워크/예외 응답은 JSON이 아닐 수 있습니다. 응답을 잃은 요청은 이미 생성되었을 수 있고 중복 방지 키가 없는 요청은 무조건 다시 보내지 마세요. 같은 세션의 시안 목록에서 먼저 확인하세요.
앞으로 추가될 기능
아직 제공하지 않는 API 보기
아래는 미제공 기능입니다. 요청 계약·실행 예제·설정 링크는 구현 및 검증 후 공개합니다. 현재 키 발급만으로 활성화되지 않습니다.
| 기능 | 상태 |
|---|---|
| 결제·구독 변경 외부 웹훅 | 미제공 |
처음이라면 카페나 공방 소개를 한 문장으로 적어 보세요. 그 내용을 바탕으로 웹에서 시안을 먼저 확인해도 좋아요.