자체 PBX의 API 및 MCP
자체 PBX Cenaly는 여러분의 프로그램과 AI 어시스턴트에 열려 있습니다:
- CRM, 회계 시스템, 자체 웹사이트 — REST API: 통화 기록, 녹음 및 녹취록, 클릭 투 콜, 내선, 착신 전환, 통화 규칙;
- Claude Code, Claude Desktop, Cursor — 키로 연결하는 원격 MCP 서버;
- ChatGPT, claude.ai, Perplexity, Cursor 및 VS Code — 동일한 MCP 서버를 키 없이 사용: 본인 계정에 로그인하고 접근을 허용합니다(OAuth).
모든 것은 계정 소유자 명의로, 소유자가 부여한 권한 내에서만 작동합니다. 모든 변경 사항은 키 또는 앱 이름과 함께 PBX 작업 로그에 표시됩니다.
주소#
| 항목 | 주소 |
|---|---|
| REST API | https://api.cenaly.com/pbx-api/v1 |
| MCP 서버 (streamable HTTP) | https://api.cenaly.com/pbx-api/mcp |
| 메서드 문서 | api.cenaly.com/pbx-api/v1/docs |
| OpenAPI 3.1 명세 | api.cenaly.com/pbx-api/v1/openapi.json |
| OAuth 메타데이터 | https://api.cenaly.com/.well-known/oauth-authorization-server/pbx-api |
키와 권한#
키는 계정 소유자만 생성할 수 있습니다: 텔레포니 → API 및 MCP → 키 → 키 생성. "PBX 관리자" 역할의 직원은 키를 볼 수 없습니다 — 키는 소유자 명의로 작동합니다.
cpx_…형식의 키는 생성 직후 한 번만 표시되며, 연결용으로 준비된 코드 조각과 함께 제공됩니다. 당사에는 키의 지문(SHA-256)만 저장되므로 키를 복원할 수 없으며, 새로 발급하는 것만 가능합니다.- 키는 지점(지정하지 않으면 향후 생성될 지점을 포함한 모든 지점)과 기간(30일, 90일, 365일 또는 무기한)으로 제한할 수 있습니다.
- **"권한"**은 키의 권한과 지점을 즉시 변경합니다: 통합은 계속 작동하며, 새 권한은 다음 요청부터 적용됩니다.
- **"폐기"**는 즉시 그리고 영구적으로 적용됩니다: 해당 키를 사용한 다음 요청은 401을 받습니다.
- 계정당 활성 키 및 연결은 최대 20개입니다.
권한은 미리 준비된 세트 또는 개별 체크박스로 선택합니다:
| 권한 | 제공 내용 | 세트 |
|---|---|---|
calls:read |
통화 기록 및 경로가 포함된 통화 카드 | CRM, AI 어시스턴트, 읽기 전용, 전체 |
recordings:read |
통화 녹음 링크(10분) 및 녹취록 | CRM, AI 어시스턴트, 읽기 전용, 전체 |
calls:dial |
클릭 투 콜: 먼저 직원의 전화가 울리고, 응답 후 PBX가 번호를 다이얼합니다 | CRM, AI 어시스턴트, 전체 |
extensions:read |
내선, 그룹, 착신 전환, "방해 금지" | CRM, AI 어시스턴트, 읽기 전용, 전체 |
extensions:write |
개인 착신 전환 및 "방해 금지" 변경 | AI 어시스턴트, 전체 |
presence:read |
누가 통화 가능한지, 통화 중인지, 통화하고 있는지 | AI 어시스턴트, 읽기 전용, 전체 |
reports:read |
기간별 콜센터 지표: 서비스 수준, 대기, 포기, AHT, FCR, CSAT, 통화 손실 지점, 대기열·회선·직원별 분석 | 읽기 전용, 전체 |
routes:read |
번호, 인바운드 및 아웃바운드 규칙, 규칙 이력 | 읽기 전용, 전체 |
routes:write |
통화 규칙 생성, 변경, 삭제, 순서 변경, 이력에서 복원 | 별도 체크박스만 |
config:export |
PBX 설정을 파일 형식으로 내보내기 | 별도 체크박스만 |
config:import |
테이블에서 설정 가져오기: 미리보기 후 적용 | 별도 체크박스만 |
webhooks:write |
통화 이벤트 웹훅 생성, 변경, 테스트, 삭제 | 별도 체크박스만 |
daynight:write |
지점을 당일 종료 시까지 또는 특정 날짜까지 닫기, 열기 — 전화기의 *28과 동일 |
별도 체크박스만 |
무언가를 변경하거나 통화 시간을 소모하는 권한은 창에서 황색으로 표시됩니다. 마지막 세 권한은 "전체"를 포함한 어떤 세트에도 포함되지 않습니다 — 별도 체크박스로만 의식적으로 부여할 수 있습니다.
ChatGPT, claude.ai, Perplexity, Cursor, VS Code: 키 없이 연결#
클라우드 어시스턴트는 헤더에 키를 삽입할 수 없으므로 OAuth로 MCP 서버를 연결합니다. 키를 생성할 필요가 없습니다.
claude.ai: 설정 → 커넥터 → 자체 커넥터 추가 → https://api.cenaly.com/pbx-api/mcp 입력 → 연결.
ChatGPT: 설정 → 앱 및 커넥터 → 고급 → 개발자 모드 활성화 → 생성 → https://api.cenaly.com/pbx-api/mcp 입력, 인증 — OAuth.
Perplexity: 계정 설정 → 커넥터 → "+ 자체 커넥터" → 원격 → https://api.cenaly.com/pbx-api/mcp 입력, 인증 — OAuth.
Cursor 및 VS Code: 키 헤더 없이 https://api.cenaly.com/pbx-api/mcp 주소로 원격(HTTP) MCP 서버를 추가하세요 — 로그인 창이 브라우저에서 열립니다. Cursor: .cursor/mcp.json → {"mcpServers": {"cenaly-pbx": {"url": "https://api.cenaly.com/pbx-api/mcp"}}}; VS Code: "MCP: Add Server" 명령 → HTTP.
이후 어시스턴트가 Cenaly 로그인 창을 엽니다. 로그인 후 다음이 표시됩니다:
- 누가 접근을 요청하는지 — 앱은 응답이 반환될 주소로 이름이 지정됩니다: "검증된 커넥터 · chatgpt.com"(claude.ai, perplexity.ai, vscode.dev, Cursor도 동일), "이 컴퓨터의 프로그램" 또는 "알 수 없는 주소" 경고. 앱이 보낸 이름은 작게 표시됩니다 — 누구든 작성할 수 있습니다;
- 권한 — 앱이 요청한 항목이 표시됩니다(앱이 "모든 것"을 요청하면 "AI 어시스턴트" 세트가 표시됨); "별도 체크박스만" 권한은 사전에 절대 표시되지 않습니다;
- 지점 — 전체 또는 선택.
"허용"을 누르면 어시스턴트로 돌아가며, 어시스턴트는 즉시 PBX 도구를 볼 수 있습니다. 연결은 텔레포니 → API 및 MCP → 키에 OAuth 라벨과 앱 주소가 있는 행으로 나타납니다 — 여기에서 권한을 변경하고 접근을 폐기할 수 있습니다. 동일한 앱을 다시 연결하면 두 번째 연결이 생성되는 것이 아니라 권한이 갱신됩니다.
연결을 허용할 수 있는 사람은 계정 소유자뿐입니다.
Claude Code, Claude Desktop, Cursor: 키로 연결#
Claude Code — 명령 하나로:
claude mcp add --transport http telephony https://api.cenaly.com/pbx-api/mcp --header "Authorization: Bearer cpx_…"
Claude Desktop, Cursor (.cursor/mcp.json) 및 설정 파일을 사용하는 기타 클라이언트:
{
"mcpServers": {
"telephony": {
"type": "http",
"url": "https://api.cenaly.com/pbx-api/mcp",
"headers": { "Authorization": "Bearer cpx_…" }
}
}
}
키를 생성하면 해당 키가 포함된 붙여넣기용 코드가 바로 표시됩니다. AI 어시스턴트는 키로 허용된 도구만 보며, "오늘 누가 전화했는데 연결되지 않았나", "안나의 전화로 이 고객에게 다시 전화해 줘", "내 휴대폰으로 착신 전환을 켜 줘"에 답할 수 있습니다.
REST API#
키는 Authorization: Bearer <키> 헤더에 넣습니다(또는 X-Api-Key). 키에 지점이 하나면 location 파라미터를 전달하지 않아도 되고, 여러 개면 400 location_required 응답이 지점 목록을 나열합니다.
| 메서드 | 경로 | 권한 | 하는 일 |
|---|---|---|---|
| GET | /v1/me |
— | 키의 권한, 지점, 한도 |
| GET | /v1/locations |
— | 키의 지점과 각 지점의 PBX |
| GET | /v1/calls |
calls:read |
통화 기록: since, until, direction, status (answered, missed, ai), number, extension, limit 최대 100, cursor |
| GET | /v1/calls/{id} |
calls:read |
경로가 포함된 통화: 어떤 규칙이 작동했는지, 누구의 전화가 울렸는지, 누가 응답했는지 |
| GET | /v1/calls/{id}/recording |
recordings:read |
10분 동안 유효한 녹음 링크 |
| GET | /v1/calls/{id}/transcript |
recordings:read |
통화 녹취록과 요약 |
| POST | /v1/dial |
calls:dial |
클릭 통화: { "extension": "101", "number": "+995555123456" } |
| GET | /v1/dial/{commandId} |
calls:dial |
클릭 통화 진행 상황 |
| GET | /v1/extensions |
extensions:read |
내선, 그룹, 착신 전환, "방해 금지" |
| PUT | /v1/extensions/{ext}/forwarding |
extensions:write |
내선의 개인 착신 전환 |
| PUT | /v1/extensions/{ext}/dnd |
extensions:write |
"방해 금지": { "on": true } |
| GET | /v1/presence |
presence:read |
지금 통화 가능한 사람 |
| GET | /v1/metrics |
reports:read |
기간별 콜센터 지표: from, to (지점의 현지 날짜 YYYY-MM-DD, 기본값 — 최근 7일), compare (prev, year, prev,year 또는 비움), sections (metrics,loss,queues,lines,operators,directions,heatmap,daily), callbacks=true — 재통화 속도도 포함 |
| GET | /v1/routes |
routes:read |
번호, 수신 및 발신 규칙 |
| GET | /v1/routes/history |
routes:read |
규칙 변경 이력 |
| POST, PUT, DELETE | /v1/routes/inbound…, /v1/routes/outbound… |
routes:write |
통화 규칙을 하나씩: 생성, 전달된 필드 변경, 삭제, 순서 변경, 롤백 |
| GET, POST | /v1/config/export, /v1/config/import |
config:export, config:import |
파일 형식의 설정; 업로드는 미리보기 후 적용 |
| GET | /v1/events |
calls:read |
지점의 7일간 통화 이벤트(최대 2000건): since (이벤트 id 또는 시간), types, limit (응답당 최대 500) |
| GET, POST, PATCH, DELETE | /v1/webhooks… |
webhooks:write |
웹훅: 목록, 생성, 변경, 삭제; …/rotate-secret, …/test, …/deliveries |
| GET, PUT | /v1/open-closed |
routes:read, daynight:write |
휴무 여부와 종료 시각; 당일 자정까지 휴무 { "closed": true }, 특정 날짜까지 { "closed": true, "until": "2026-10-05T09:00" } 또는 영업 재개 |
필드 전체 설명은 문서 페이지와 OpenAPI에 있습니다.
오늘 누가 전화했는데 연결되지 않았는지:
curl -H "Authorization: Bearer cpx_…" \
"https://api.cenaly.com/pbx-api/v1/calls?status=missed&since=$(date -u +%Y-%m-%dT00:00:00Z)"
내선 101의 전화로 고객에게 다시 전화하기:
curl -X POST -H "Authorization: Bearer cpx_…" -H "Content-Type: application/json" \
-d '{"extension":"101","number":"+995555123456"}' "https://api.cenaly.com/pbx-api/v1/dial"
9월 지표를 8월 및 작년 9월과 비교 — 자체 BI 패널용:
curl -H "Authorization: Bearer cpx_…" \
"https://api.cenaly.com/pbx-api/v1/metrics?from=2026-09-01&to=2026-09-30&compare=prev,year"
응답의 metrics에는 텔레포니 → 통화 기록 → 분석 화면과 동일한 수치가 담깁니다: 수신, 응답 및 부재중, 서비스 수준(임계값과 목표 — 대기열 설정에서 가져옴), 평균 및 최대 대기 시간, 짧은 통화를 제외한 포기 비율, AHT, FCR, 전환 및 봇 비율, CSAT. compare.prev와 compare.year — 이전 기간과 작년 동일 기간의 같은 필드입니다. 작년과의 비교는 최대 400일 기간에 대해 계산됩니다.
직원이 15초 안에 응답하지 않으면 휴대폰으로 착신 전환 켜기:
curl -X PUT -H "Authorization: Bearer cpx_…" -H "Content-Type: application/json" \
-d '{"forward":{"noAnswer":{"kind":"number","number":"+995599000111"},"noAnswerSec":15}}' \
"https://api.cenaly.com/pbx-api/v1/extensions/101/forwarding"
MCP 도구#
REST와 동일한 작업입니다. 클라이언트는 키 또는 연결로 허용된 도구만 볼 수 있습니다:
| 도구 | 권한 |
|---|---|
list_locations |
— |
list_calls, get_call |
calls:read |
get_call_recording, get_call_transcript |
recordings:read |
call_number, get_dial_status |
calls:dial |
list_extensions |
extensions:read |
set_forwarding, set_do_not_disturb |
extensions:write |
get_presence |
presence:read |
get_call_metrics |
reports:read |
list_routes, list_route_history |
routes:read |
save_inbound_rule, delete_inbound_rule, move_inbound_rule, save_outbound_rule, delete_outbound_rule, move_outbound_rule, set_outbound_calls, restore_route_version |
routes:write |
export_config |
config:export |
import_config |
config:import |
get_call_events |
calls:read |
list_webhooks, save_webhook, delete_webhook, rotate_webhook_secret, test_webhook, list_webhook_deliveries |
webhooks:write |
get_open_closed |
routes:read |
set_open_closed |
daynight:write |
서버는 세션 없이 작동합니다(streamable HTTP, JSON-RPC 2.0): initialize, tools/list, tools/call.
자체 커넥터 개발자를 위한 OAuth#
인증 서버는 MCP 인증 사양을 따르므로, 이를 지원하는 클라이언트라면 모두 사용할 수 있습니다:
- 토큰 없이 MCP에 요청하면
WWW-Authenticate: Bearer resource_metadata="https://api.cenaly.com/.well-known/oauth-protected-resource/pbx-api/mcp"헤더와 함께 401을 받습니다. - 리소스 메타데이터(RFC 9728)는 인증 서버를 알려줍니다 —
https://api.cenaly.com/pbx-api; 해당 메타데이터(RFC 8414)는https://api.cenaly.com/.well-known/oauth-authorization-server/pbx-api(및…/pbx-api/.well-known/openid-configuration)에 있습니다. - 동적 클라이언트 등록(RFC 7591):
POST /pbx-api/oauth/register. 반환 주소는localhost/127.0.0.1(모든 포트) 또는 알려진 커넥터의 주소(claude.ai, claude.com, chatgpt.com, chat.openai.com, vscode.dev, www.perplexity.ai, enterprise.perplexity.ai; Cursor의 경우 정확히cursor://anysphere.cursor-mcp/oauth/callback만)여야 합니다. 클라이언트 인증 방식은none(공개 클라이언트),client_secret_post또는client_secret_basic입니다. - PKCE를 사용한 인증 코드(
S256만):GET /pbx-api/oauth/authorize→ 소유자의 로그인 및 동의 창 →code,state,iss와 함께 지정한 주소로 반환.resource매개변수는 MCP 서버 주소입니다. - 토큰:
POST /pbx-api/oauth/token. 코드는 일회용이며 2분 동안 유효합니다(잘못된code_verifier는 코드를 무효화합니다); 액세스 토큰은 1시간; 리프레시 토큰은 90일이며, 갱신할 때마다 새 토큰이 발급되고 이전 토큰은 작동을 멈춥니다. - 철회:
POST /pbx-api/oauth/revoke(RFC 7009)는 연결을 통째로 무효화합니다 — “API 및 MCP” 창의 “철회”와 같습니다.
액세스 토큰은 키가 사용되는 모든 곳에서 허용됩니다: MCP와 REST 모두에서, 소유자가 허용한 권한과 지점으로 작동합니다.
한도 및 오류#
- 키 또는 연결당: 분당 120회 요청 및 일 20,000회(REST와 MCP 합산).
- 클릭 통화는 별도: 분당 10회 및 일 300회.
- 로그는 요청당 최대 100통화; PBX의 지점 로그에는 최근 90일간의 최근 400통화가 표시됩니다.
| 코드 | 오류 | 해결 방법 |
|---|---|---|
| 401 | api_key_missing, api_key_invalid |
키가 없거나, 철회되었거나, 만료되었습니다; OAuth 토큰이 만료되었으면 갱신하십시오 |
| 403 | scope_required |
키에 권한이 없습니다 — “권한”에서 추가하십시오 |
| 400 | location_required |
키에 지점이 여러 개 있습니다 — location을 전달하십시오 |
| 404 | location_not_found |
지점이 없거나 키가 다른 지점으로 제한되어 있습니다 |
| 429 | rate_limited |
한도가 소진되었습니다. Retry-After초 후에 다시 시도하십시오 |
통화 이벤트#
실시간 — PBX 웹훅: 텔레포니 → API 및 MCP → 웹훅 또는 webhooks:write 권한이 있는 키로 POST /v1/webhooks. 귀하의 https 주소로 몇 초 만에 각 이벤트에 대한 POST가 도착합니다: call.started (시작됨: 방향, 발신자, 수신자, 회선), call.ringing (누구에게 울리는지 — 전화기마다 하나씩), call.answered (누가 응답했는지), call.transferred (누가 누구에게 전환했는지), call.ended (결과, 통화 시간, 대화, 응답자). 웹훅은 지점과 이벤트 유형으로 제한할 수 있습니다.
{ "id": "1727771234.56:2", "type": "call.answered", "createdAt": "2026-10-01T10:00:05Z",
"locationId": "…", "webhookId": "wh_…", "attempt": 1,
"data": { "callId": "1727771234.56", "seq": 2, "direction": "inbound",
"from": { "number": "+995555123456" }, "party": { "extension": "101", "name": "Анна" } } }
서명 — 헤더 X-Cenaly-Signature: t=<unix 시간>,v1=<"t.본문"에 대한 HMAC-SHA256, 웹훅 시크릿 사용>. 재계산하고, 상수 시간으로 비교하며, 5분보다 오래된 t는 거부합니다. 시크릿 whsec_…은 한 번만 표시되며, "새 시크릿"은 이전 시크릿을 즉시 무효화합니다.
전달 — 10초 이내에 2xx로 응답합니다. 그렇지 않으면 10초, 30초, 2, 5, 10, 15, 15분 간격으로 재시도합니다 (8회 시도, 약 1시간). 재시도는 동일한 id를 가지므로 이를 기준으로 중복을 걸러냅니다. 전달 로그 (응답 코드, 시간, 시도) — 웹훅 창과 GET /v1/webhooks/{id}/deliveries에서 확인할 수 있으며, "확인"은 서비스 webhook.ping을 전송합니다. 주소가 3일 연속 단 하나의 전달도 받지 못하면 웹훅이 자동으로 꺼집니다: 소유자에게 이메일이 발송되고, "웹훅" 창에 "자동으로 꺼짐" 표시와 "켜기" 버튼이 나타납니다 (또는 { "enabled": true }와 함께 PATCH /v1/webhooks/{id}).
자체 서버 없이 — 동일한 피드를 폴링으로: GET /v1/events?since=<마지막 이벤트 id> 또는 MCP 도구 get_call_events (지점의 최근 7일 이벤트, 최대 2000건, 응답당 최대 500건, more: true이면 동일한 since로 계속 조회).
녹음 처리 후 — 녹취 및 분석이 포함된 통화는 “통화” 섹션의 “웹훅” 소스 웹훅으로 도착합니다.
“열림 / 닫힘” — { "closed": true }와 함께 PUT /v1/open-closed를 호출하면 지점이 당일 자정까지 닫힙니다 (전화기의 *28과 동일: 통화는 “운영 시간 외” 규칙으로 처리됨), false는 엽니다. GET은 닫혔는지, 누가 닫았는지 보여줍니다. "until"을 사용하면 해당 시점까지 닫히며 (최대 30일 이내), 이후 자동으로 열립니다: 오프셋 없는 시간 (2026-10-05T09:00)은 지점의 시간대로 해석됩니다. false는 이것도 해제합니다. 라우팅에서 “*28 코드로 닫기” 체크박스가 활성화되어 있어야 합니다 (그렇지 않으면 409).
보안#
- 키는 한 번만 표시되며, 당사에는 지문만 저장됩니다. 유출된 키는 버튼 하나로 폐기할 수 있습니다.
- OAuth 연결은 소유자만 승인할 수 있으며, 어느 주소로 접근 권한이 부여되는지 확인할 수 있습니다. 승인된 연결은 권한과 지점으로 제한할 수 있고 언제든지 취소할 수 있습니다.
- 전화번호는 귀하 고객의 개인정보입니다: 통화 기록과 녹음에 대한 권한은 실제로 필요한 통합에만 부여하십시오.
- 키 또는 연결을 통한 모든 변경 사항은 키 또는 앱 이름과 함께 PBX 작업 로그에 기록됩니다.