---
title: HTTP API 레퍼런스
description: 공개 API raw 엔드포인트 — JS 외 스택이나 저수준 연동용
---

# HTTP API 레퍼런스

베이스 URL: `https://api.roottale.com`
인증: 모든 요청에 `Authorization: Bearer rtlk_cust_...` 헤더.

JS/TS 스택은 raw 호출 대신 `@roottale/cms-client`를 사용하세요 — 에러 처리,
브라우저 노출 방지, 전화번호 포맷 등이 포함되어 있습니다.

## GET /v1/cms/public/posts

발행된 글 목록 (커서 페이지네이션).

| 쿼리 | 설명 |
|---|---|
| `limit` | 페이지 크기 (서버에서 상한 clamp) |
| `cursor` | 이전 응답의 `next_cursor` |
| `type` | `post` \| `page` |
| `site_id` | 멀티 사이트 키일 때만 — site-scoped 키면 생략 |

```json
{
  "items": [ { "id": "…", "slug": "…", "title": "…", "body_json": {…},
               "terms": [{ "taxonomy": "category", "name": "…", "slug": "…" }],
               "published_at": "…" } ],
  "has_more": false,
  "next_cursor": null
}
```

## GET /v1/cms/public/posts/{identifier}

글 1개 — `identifier`는 slug 또는 UUID. 미발행/없는 글은 `404`.

## GET /v1/cms/public/search

발행된 글 키워드 검색 (사이트 내 검색, WP `?s=` 패리티). title·excerpt·본문
텍스트의 case-insensitive 부분일치, 최신 발행순. 응답은 카드 렌더용 슬림
hit — 본문(`body_json`)은 미포함이므로 상세는 slug 로 글 1개 API를 호출하세요.

| 쿼리 | 설명 |
|---|---|
| `q` | 검색 키워드 (필수, 1~100자) |
| `limit` | 결과 수 1~50, 기본 10 |
| `type` | `post`(기본) \| `page` |
| `site_id` | 멀티 사이트 키일 때만 |

```json
{ "tenant_id": "…", "site_id": "…", "query": "세무",
  "items": [ { "id": "…", "type": "post", "title": "…", "slug": "…",
               "excerpt": "…", "featured_media_url": "…",
               "published_at": "…" } ] }
```

JS/TS 는 `@roottale/cms-client/server` 의 `searchPosts({ apiKey, query })` 를
사용하세요 — 구 서버(라우트 미배포)의 404 를 빈 배열로 처리합니다.

## GET /v1/cms/public/menus

네비게이션 메뉴 전체 — 어드민 "디자인 > 메뉴" 저장값. 항목은 깊이 2 트리.

```json
{ "tenant_id": "…", "site_id": "…",
  "items": [ { "id": "…", "name": "헤더 메뉴", "slug": "primary",
               "items": [ { "id": "…", "label": "회사 소개", "url": "/about",
                            "children": [ { "id": "…", "label": "오시는 길",
                                            "url": "/about/location" } ] },
                          { "id": "…", "label": "블로그", "url": "/blog" } ],
               "updated_at": "…" } ] }
```

## GET /v1/cms/public/menus/{slug}

위치 핸들(`primary`, `footer` 등)로 메뉴 1개. 없으면 `404` — 사이트는 자체
fallback 네비를 렌더하세요 (`menus.md` 참고).

## GET /v1/cms/public/theme

어드민에서 설정한 디자인 토큰. 설정된 그룹만 포함됩니다.

```json
{ "tenant_id": "…", "site_id": "…",
  "colors": {…}, "fonts": {…}, "radius": {…}, "updated_at": "…" }
```

## GET /v1/cms/public/blog-settings

블로그 표시 설정 (TOC·작성자·발행일·작성자 카드, 저자 프로필).

```json
{ "show_table_of_contents": false, "show_author": true, "show_date": true,
  "show_author_card": true, "toc_title": null,
  "author_profile_name": null, "author_profile_bio": null,
  "author_profile_image_url": null, "author_profile_image_radius": "circle",
  "updated_at": null }
```

## GET /v1/cms/public/business-profile

비즈니스 프로필 (로컬 SEO) — 어드민 "운영 > 비즈니스 프로필" 저장값.
미설정이면 `configured: false` + 필드 `null`.

```json
{ "tenant_id": "…", "site_id": "…", "configured": true,
  "name": "길동세무회계", "legal_name": null,
  "business_type": "AccountingService",
  "telephone": "02-1234-5678", "email": null,
  "address": { "street_address": "테헤란로 123", "address_locality": "강남구",
               "address_region": "서울특별시", "postal_code": "06234" },
  "geo": { "latitude": 37.5006, "longitude": 127.0364 },
  "opening_hours": [ { "days": ["Mo","Tu","We","Th","Fr"],
                       "opens": "09:00", "closes": "18:00" } ],
  "price_range": "₩₩", "area_served": ["서울 강남구"],
  "profiles": { "naver_place": "https://…", "google_business": "https://…",
                "kakao_channel": null, "instagram": null, "naver_blog": null },
  "updated_at": "…" }
```

`business_type`: `LocalBusiness` | `ProfessionalService` |
`AccountingService` | `LegalService` | `MedicalClinic` | `Dentist` |
`RealEstateAgent` | `Restaurant` | `BeautySalon`.

## GET /v1/cms/public/analytics

분석 태그 설정.

```json
{ "tags": [ { "provider": "ga4", "id": "G-XXXXXXX", "enabled": true } ] }
```

`provider`: `ga4` | `clarity` | `meta_pixel` | `naver`.

## GET /v1/cms/public/jwks

웹훅 서명 검증용 site-scoped JWKS 공개키. **site-scoped 키 필수** — 테넌트
전체 키로 호출하면 `400 site_scope_required`.

## POST /v1/public/inquiries

상담문의(리드) 접수. `multipart/form-data`.

| 필드 | 필수 | 비고 |
|---|---|---|
| `vertical` | ✅ | `consulting` \| `medical` \| `tax` \| `legal` |
| `contact_name` | ✅ | |
| `business_name` | ✅ | |
| `email` | ✅ | `.+@.+\..+` |
| `phone` | ✅ | |
| `privacy_consent` | ✅ | 체크박스 (`on`) |
| `overseas_transfer_consent` | medical 시 ✅ | |
| `message`, `consultation_field`, `current_site_url` | | |
| `lead_kind` | | `patient`(기본) \| `sales` |
| `_redirect_url` | | 완료 후 redirect base (allowlist 검증) |
| `attr_landing_path`, `attr_rt_src`, `attr_utm_source`, `attr_utm_medium`, `attr_utm_campaign`, `attr_referrer`, `attr_first_touch_at` | | 유입 어트리뷰션 — CRM에 유입 경로 표시 (`inquiries.md` 참고). 그 외 `attr_*` 키는 무시 |
| 기타 임의 필드 | | 최대 50개, 암호화 보관, CRM 상세 노출 |

응답: `302` redirect — 성공 `?ok=1`, 실패 `?err=<code>`
(`consent_privacy`, `consent_overseas`, `invalid_vertical`, `missing_fields`,
`invalid_email`, `internal`). 잘못된 키는 `401`.

서버-서버 호출 시 redirect를 따라가지 말고(`redirect: "manual"`) `Location`
헤더를 파싱하세요 — `@roottale/cms-client`의 `submitInquiry`가 이를 대신합니다.

## POST /v1/cms/revalidate

수동 캐시 갱신 트리거 (등록된 웹훅으로 재발송).

```json
{ "event": "post.updated", "paths": ["/blog", "/blog/my-post"], "slug": "my-post" }
```

## 에러 형식

비 2xx 응답은 JSON 에러 바디(`code`, `message`)를 가집니다. 주요 코드:
`invalid_key`(401), `insufficient_scope`(403), `rate_limited`(429),
`not_found`(404).

## 캐시 헤더

공개 콘텐츠 응답은 private 캐시 헤더로 내려갑니다 — CDN 공유 캐시에 의존하지
말고 사이트 측 ISR + 발행 웹훅 조합을 사용하세요.
