개발자 안내 · REST와 MCP
지식 베이스 동기화
기존 시스템에서 계속 글을 쓰고 변경 사항을 Sonny로 보내세요. 이 안내에서는 Billing 카테고리를 만들고, 아티클을 초안으로 동기화한 뒤, 인증된 고객 대상 그룹에 게시해요.
1. 채널을 고르고 키 만들기
- 채널 → 내 채널을 여세요. 대시보드 URL에서 /app/sources/ 뒤의 ID를 복사하세요. API에서는 이 채널 ID를 sourceId라고 불러요. 위젯의 siteId나 도움말 센터의 공개 슬러그와는 달라요.
- 설정 → 개발자 → API 키 → 키 만들기에서 kb:read와 kb:write를 부여하세요. 채널 접근 권한은 동기화할 채널로만 제한하세요. 한 번만 표시되는 키를 서버의 시크릿 관리자에 저장하세요.
- 아래 변수를 백엔드 환경에 설정하세요. 예시는 curl을 사용해요. 대문자 ID는 Sonny가 반환한 값으로 바꾸세요. 실제 도움말 센터를 동기화하기 전에 테스트 채널에서 먼저 실행해 보세요.
# Load SONNY_API_KEY from your server's secret manager first.
# SOURCE_ID is the ID in the channel dashboard URL: /app/sources/SOURCE_ID
export SOURCE_ID="YOUR_SOURCE_ID"
export BASE="https://www.usesonny.com/api/v1/sources/$SOURCE_ID"선택 사항인 탐색: GET /api/v1/sources와 MCP 도구 list_sources는 접근 가능한 채널을 반환하며 conversations:read가 필요해요. 채널 ID를 이미 알고 있다면 이 범위는 필요 없어요. 도메인 연결은 대시보드에서만 할 수 있어요.
API 키 전체 설정 방법2. 카테고리 만들기 또는 업데이트
URL에 원본 시스템의 고정 ID를 사용하세요. 같은 외부 ID를 다시 보내면 중복을 만들지 않고 기존 카테고리를 업데이트해요. 처음 만들 때 이 채널의 도움말 센터가 자동으로 초기화돼요.
curl --fail-with-body -X PUT "$BASE/categories/by-external-id/billing" \
-H "Authorization: Bearer $SONNY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Billing","position":0}'만들면 201, 업데이트하면 200이 반환되고 카테고리는 data 아래에 있어요. Sonny 내부 카테고리 ID가 필요하면 data.id를 저장하세요. 외부 ID는 URL 인코딩하고, 제목이 바뀌어도 고정해 두세요.
3. 아티클을 초안으로 동기화
curl --fail-with-body -X PUT "$BASE/articles/by-external-id/billing-guide" \
-H "Authorization: Bearer $SONNY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"title":"Manage billing","body":"## Find your invoices\n\nOpen **Settings → Billing** in your account.","bodyFormat":"markdown","categoryExternalId":"billing","status":"draft"}'응답에는 data.id와 정제된 HTML인 data.body가 들어 있어요. 채널 → 내 채널 → 도움말 센터에서 아티클을 열어 서식을 확인하세요. Markdown은 HTML로 변환되고, bodyFormat: "html"(기본값)로 원본 HTML도 보낼 수 있어요. 아티클 제목은 본문의 제목과 별개예요.
2단계의 카테고리를 가리키려면 categoryExternalId를, Sonny ID를 쓰려면 categoryId를 쓰세요. 둘 중 하나만 보내세요. 아티클을 미분류로 만들려면 어느 쪽이든 null로 설정하세요. 모든 upsert에는 title과 body가 필요하며, 부분 수정에는 PATCH를 쓸 수 있어요. status를 생략하면 새 아티클은 초안이 돼요.
4. 접근 권한을 설정하고 게시하기
이 비공개 센터 예시에서는 먼저 인증된 고객 로그인을 연결하세요. 대상 그룹을 만들고 반환된 data.id를 저장하세요.
curl --fail-with-body -X POST "$BASE/audiences" \
-H "Authorization: Bearer $SONNY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Pro customers","rules":{"match":"all","conditions":[{"trait":"plan","op":"in","value":["pro","business"]}]}}'아래의 AUDIENCE_ID, ARTICLE_ID, 로그인 URL을 바꾸세요. 첫 번째 요청은 도움말 센터를 게시하고 Pro 고객으로 제한해요. 두 번째 요청은 아티클을 게시해요. 앱의 로그인 흐름이 서명된 고객 JWT를 도움말 센터와 교환해야 하며, signInUrl만 설정한다고 누군가 로그인되지는 않아요.
curl --fail-with-body -X PATCH "$BASE/help-center" \
-H "Authorization: Bearer $SONNY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"enabled":true,"title":"Acme Help Center","audience":"verified","audienceId":"AUDIENCE_ID","signInUrl":"https://app.example.com/login"}'
# Replace ARTICLE_ID and AUDIENCE_ID with the returned data.id values.
curl --fail-with-body -X PATCH "$BASE/articles/ARTICLE_ID" \
-H "Authorization: Bearer $SONNY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"audienceId":"AUDIENCE_ID","status":"published"}'GET /help-center가 반환한 URL로 로그인한 독자 화면과 로그아웃한 브라우저를 모두 테스트하세요. API 키와 MCP는 직원으로 동작해 범위 안의 콘텐츠를 읽을 수 있으므로, 요청이 성공했다고 고객이 아티클을 볼 수 있다는 뜻은 아니에요.
공개 센터라면 센터와 아티클에 audience: "everyone"과 audienceId: null을 쓰고 카테고리의 접근 권한도 확인하세요. 이름 붙은 그룹만 빼려면 audienceId: null을 보내세요. audience를 명시적으로 바꾸지 않는 한 인증된 고객 전용 접근은 유지돼요. audienceId를 생략하면 이전 지정이 유지돼요. 도움말 센터, 카테고리, 아티클의 모든 제한을 만족해야 해요.
이름 붙은 규칙은 match: "all" 또는 "any"를 쓰고, 조건은 최대 10개이며, 값은 엄격하게 타입을 따져요. 연산자: eq, neq, in, not_in, exists, not_exists, gt, gte, lt, lte. 숫자 비교에는 숫자를, in/not_in에는 스칼라 값 1–100개로 된 배열을 쓰고, exists/not_exists에는 value를 생략하세요. 없는 특성은 비교를 통과하지 못해요. 규칙 동작, 상속, 접근 문제 해결 보기.
5. 동기화를 최신으로 유지하기
콘텐츠를 중복하지 않고 반복해서 쓰기
원본이 바뀔 때마다 같은 외부 ID로 PUT을 반복하세요. 외부 ID는 도움말 센터 안에서 고유해요. 업데이트에서 생략한 필드는 현재 값을 유지해요. 이후 upsert에서 status를 생략하면 게시 상태가 유지되고, draft를 명시적으로 보내면 게시된 아티클이 내려가요. publishedAt은 처음 게시할 때 설정되고 바뀌지 않아요. 읽기 전용이라 과거 게시 날짜를 가져올 수는 없어요. 바뀌지 않은 콘텐츠와 메타데이터만 수정한 경우에는 불필요한 재임베딩이 일어나지 않아요.
변경 사항과 분석 읽기
curl --fail-with-body --get "$BASE/articles" \
-H "Authorization: Bearer $SONNY_API_KEY" \
--data-urlencode "page=1" --data-urlencode "limit=100" \
--data-urlencode "status=published" \
--data-urlencode "updatedSince=2026-09-01T00:00:00Z"
# Get the full sanitized HTML and read-only analytics for one article.
curl --fail-with-body "$BASE/articles/ARTICLE_ID" \
-H "Authorization: Bearer $SONNY_API_KEY"아티클과 카테고리 목록은 data와 함께 pagination(page, limit, total)을 반환해요. 페이지는 1부터 시작하고, limit 기본값은 50이며 최대 100까지 받아요. 같은 필터를 유지하며 page × limit이 total에 도달할 때까지 페이지를 요청하세요. 두 목록 모두 externalId와 updatedSince를 받고, 아티클 목록은 categoryId와 status(draft 또는 published)도 받아요. updatedSince에는 시간대가 포함된 ISO 타임스탬프를 쓰세요. 그 시각에 업데이트된 레코드도 포함돼요.
각 아티클 상세를 읽어 전체 body, viewCount, helpfulYes, helpfulNo를 얻으세요. 이 분석 값은 읽기 전용이에요. updatedSince는 현재 레코드를 나열할 뿐 삭제는 알려 주지 않아요. 원본 시스템에서 삭제를 추적하고, 필요할 때 해당 Sonny 레코드를 명시적으로 삭제하세요.
이미지 업로드
curl --fail-with-body -X POST "$BASE/media" \
-H "Authorization: Bearer $SONNY_API_KEY" \
-F "file=@billing.png;type=image/png"반환된 data.url을 Markdown 이미지 문법이나 HTML img 요소에 넣은 뒤 아티클 본문을 동기화하세요. 업로드는 최대 25MB의 JPEG, PNG, GIF, WebP를 받으며, 초기화된 도움말 센터가 필요해요. 업로드한 URL은 공개되며, 아티클의 대상 제한이 이미지 URL을 보호하지는 않아요. MCP는 multipart form data 대신 sourceId, fileName, contentType, 파일의 dataBase64와 함께 upload_kb_media를 사용해요.
아티클과 카테고리 정렬
curl --fail-with-body -X PUT "$BASE/articles/reorder" \
-H "Authorization: Bearer $SONNY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"ids":["ARTICLE_ID_1","ARTICLE_ID_2"]}'먼저 status, category, 날짜 필터 없이 초안을 포함한 모든 페이지를 가져오세요. 샘플 ID를 현재 모든 아티클 ID로 원하는 순서대로 정확히 한 번씩 바꾸세요. 카테고리 전체 목록은 /categories/reorder를 쓰세요. 정렬에 성공하면 본문 없이 204가 반환돼요. 목록이 바뀌었거나 다른 곳의 ID, 중복 ID, 누락된 ID가 있으면 목록을 새로 고친 뒤 다시 시도하세요. 개별 쓰기에서 음수가 아닌 position을 지정할 수도 있어요.
MCP로 같은 워크플로 사용하기
- MCP 클라이언트를 연결하고 올바른 워크스페이스를 고르세요. kb:read와 kb:write가 있고 채널에 접근할 수 있는지 확인하세요.
- 대시보드의 채널 ID를 쓰거나, 자격 증명에 conversations:read도 있다면 list_sources를 호출하세요. 위젯의 siteId로 대신하면 안 돼요.
- 아래 처음 다섯 예시를 순서대로 실행하세요. SOURCE_ID는 채널 ID로, ARTICLE_ID/AUDIENCE_ID는 앞선 결과의 data.id로 바꾸세요. 게시하기 전에 로그인 URL을 바꾸고 로그인을 연결하세요.
- list_kb_articles로 변경 사항을 검토하세요. 초안을 포함해 필터 없는 전체 목록을 모은 뒤에만 정렬을 실행하세요. 쓰기에 성공하면 목록을 새로 고치세요.
각 예시는 도구 이름과 인수를 보여 줘요. upsert 필드는 arguments에 바로 넣고, update 도구는 바뀐 필드를 updates 안에 넣어요. MCP 엔드포인트로 보내는 HTTP 요청이 아니라 도구 호출 입력이에요.
1. upsert_kb_category
{
"name": "upsert_kb_category",
"arguments": {
"sourceId": "SOURCE_ID",
"externalId": "billing",
"name": "Billing",
"position": 0
}
}2. upsert_kb_article
{
"name": "upsert_kb_article",
"arguments": {
"sourceId": "SOURCE_ID",
"externalId": "billing-guide",
"title": "Manage billing",
"body": "## Find your invoices\n\nOpen **Settings → Billing** in your account.",
"bodyFormat": "markdown",
"categoryExternalId": "billing",
"status": "draft"
}
}3. create_kb_audience
{
"name": "create_kb_audience",
"arguments": {
"sourceId": "SOURCE_ID",
"name": "Pro customers",
"rules": {
"match": "all",
"conditions": [
{
"trait": "plan",
"op": "in",
"value": [
"pro",
"business"
]
}
]
}
}
}4. update_help_center
{
"name": "update_help_center",
"arguments": {
"sourceId": "SOURCE_ID",
"updates": {
"enabled": true,
"title": "Acme Help Center",
"audience": "verified",
"audienceId": "AUDIENCE_ID",
"signInUrl": "https://app.example.com/login"
}
}
}5. update_kb_article
{
"name": "update_kb_article",
"arguments": {
"sourceId": "SOURCE_ID",
"articleId": "ARTICLE_ID",
"updates": {
"audience": "verified",
"audienceId": "AUDIENCE_ID",
"status": "published"
}
}
}6. list_kb_articles
{
"name": "list_kb_articles",
"arguments": {
"sourceId": "SOURCE_ID",
"page": 1,
"limit": 100,
"updatedSince": "2026-09-01T00:00:00Z"
}
}7. reorder_kb_articles
{
"name": "reorder_kb_articles",
"arguments": {
"sourceId": "SOURCE_ID",
"ids": [
"ARTICLE_ID_1",
"ARTICLE_ID_2"
]
}
}8. update_kb_audience
{
"name": "update_kb_audience",
"arguments": {
"sourceId": "SOURCE_ID",
"audienceId": "AUDIENCE_ID",
"updates": {
"name": "Paid customers"
}
}
}관련 작업
아래 REST 경로는 BASE 기준 상대 경로예요. 모든 필드와 응답 스키마는 API 레퍼런스를 참고하세요.
| 작업 | REST | MCP |
|---|---|---|
| 아티클 목록 / 조회 | GET /articles · GET /articles/{articleId} | list_kb_articles · get_kb_article |
| 카테고리 목록 / 조회 | GET /categories · GET /categories/{categoryId} | list_kb_categories · get_kb_category |
| 외부 ID 없이 만들기 | POST /articles · POST /categories | create_kb_article · create_kb_category |
| 기존 콘텐츠 수정 | PATCH /articles/{articleId} · PATCH /categories/{categoryId} | update_kb_article · update_kb_category |
| 카테고리 정렬 | PUT /categories/reorder | reorder_kb_categories |
| 도움말 센터 설정 읽기 / 수정 | GET /help-center · PATCH /help-center | get_help_center · update_help_center |
| 대상 그룹 목록 / 수정 | GET /audiences · PATCH /audiences/{audienceId} | list_kb_audiences · update_kb_audience |
| 삭제 | DELETE /articles/{articleId} · /categories/{categoryId} · /audiences/{audienceId} | delete_kb_article · delete_kb_category · delete_kb_audience |
아티클 삭제는 영구적이에요. 카테고리를 삭제하면 아티클이 미분류가 되고 상속된 카테고리 제한도 사라져요. 대상 그룹을 삭제하면 특성 규칙이 사라지고 연결된 콘텐츠는 인증된 고객 전용으로 남아요. 삭제하기 전에 영향받는 접근 권한을 검토하고, 아티클을 보관하고 싶다면 초안으로 내리세요.
동기화 문제 해결
- 400 · 잘못된 요청
- 필수 title/body 또는 name, 정확한 필드 이름, 올바른 규칙 타입, 완전한 정렬 ID를 확인하세요. categoryId와 categoryExternalId는 둘 중 하나만 보내세요.
- 401 · 인증되지 않음
- 유효한 Bearer 키를 보내세요. 키가 만료되거나 폐기되지 않았는지 확인하세요.
- 403 · 금지됨
- 키의 범위, 키 소유자의 권한, 워크스페이스 결제 상태를 확인하세요. MCP 클라이언트의 도구가 읽기 전용이라면 필요한 권한으로 다시 연결하세요.
- 404 · 찾을 수 없음
- 소스가 선택한 워크스페이스에 속하고 자격 증명이 허용하는지 확인하세요. 새 도움말 센터는 먼저 콘텐츠를 만들어 초기화하세요. 읽기만으로는 만들어지지 않아요.
- 409 · 충돌
- 현재 콘텐츠를 다시 가져와 충돌하는 ID나 오래된 순서를 맞춘 뒤 다시 시도하세요.
- 413 · 너무 큼
- 이미지를 25MB 한도 미만으로 줄이세요.
- 429 · 요청 한도 초과
- Retry-After만큼 기다린 뒤 다시 시도하세요. RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset으로 요청 속도를 조절하세요.