--- title: "CLOVA Chatbot Custom API" slug: "ai-application-service-chatbot-chabot" tags: ["CLOVA Chatbot", "Invoke URL", "Secret Key"] updated: 2026-09-17T09:10:00Z published: 2026-09-17T09:10:00Z canonical: "api-fin.ncloud-docs.com/ai-application-service-chatbot-chabot" --- > ## Documentation Index > Fetch the complete documentation index at: https://api-fin.ncloud-docs.com/llms.txt > Use this file to discover all available pages before exploring further. # CLOVA Chatbot Custom API Custom Chatbot으로 연동하려면 **생성한 Stage의 Invoke URL과 Secret Key를 복사하여 보관합니다.** ## 요청 Custom 채널에서 CLOVA Chatbot 으로 요청(Request)을 보낼 시 방법을 설명합니다. - Http Method: POST - Headers | name | value | | --- | --- | | Content-Type | "application/json;UTF-8" | | X-NCP-CHATBOT_SIGNATURE | signature of request body | ## 요청 Signature 헤더 - Signature 정의(X-NCP-CHATBOT_SIGNATURE) - Algorithm: HmacSHA256 - SecretKey: CLOVA Chatbot 도메인 빌더에서 생성한 SecretKey 입니다. - Sign content: Reqeust body String 을 의미합니다. - `X-NCP-CHATBOT_SIGNATURE`: `X-NCP-CHATBOT_SIGNATURE' 는 챗봇에게 요청을 보내는 메시지 입니다. 즉,챗봇에게 요청을 보낼 때, 요청 헤더에 Content-Type, X-NCP-CHATBOT_SIGNATURE 이 필요하고 X-NCP-CHATBOT_SIGNATURE 는 Sign content(Request body)을 Base64 로 인코딩하여 설정합니다. ### 언어별 Signature(Sign content) 생성 예제코드 자세한 'X-NCP-CHATBOT_SIGNATURE` 설정 및 예시를 설명합니다. - js code sample ``` const HmacSHA256 = require('crypto-js/hmac-sha256'); const EncBase64 = require('crypto-js/enc-base64'); signatureHeader = HmacSHA256(requestBodyString, secretKey).toString(EncBase64); ``` - java code sample ``` byte[] secretKeyBytes = secretKey.getBytes(StandardCharsets.UTF_8); SecretKeySpec secretKeySpec = new SecretKeySpec(secretKeyBytes, "HmacSHA256"); Mac mac = Mac.getInstance("HmacSHA256"); mac.init(secretKeySpec); byte[] signature = mac.doFinal(body.getBytes(StandardCharsets.UTF_8)); String signatureHeader = Base64.getEncoder().encodeToString(signature); ``` ### 요청 바디 #### Open - 웰컴 메시지에 응답 - trigger when open messenger, will response welcome message if have set in "Messenger Connection" -> "Custom" | 구분 | Json Model - Example | | --- | --- | | Open | { "version": "v2", "userId": "U47b00b58c90f8e47428af8b7bddcda3d", "userIp": "8.8.8.8", "timestamp": 12345678, "bubbles": [ { "type": "text", "data" : { "description" : "postback text of welcome action" } } ], "event": "open" } | Open 요청 필드 상세 설명 | 필드 이름 | 데이터 타입 | 필수여부 | 설명 | | --- | --- | --- | --- | | version | string | false | version은 'v2'가 기본이며, 값이 입력되지 않은 경우 'v1'으로 설정됨 | | userId | string | true | 봇과 채팅하는 사용자의 고유 ID로 최대 256자를 넘지 않아야 함 사용자마다 고유한 userId 설정 | | userIp | string | false | - 사용자의 Ip Adress (필수X) | | timestamp | long | true | - 타임스탬프 값 January 1, 1970, 00:00:00 GMT | | bubbles | array | true | empty array "[]" or only one [Text](/docs/ai-application-service-chatbot-chabot#Text) component caused by [welcome action](/docs/ai-application-service-chatbot-chabot#Welcome) | | event | string | true | 이벤트 value 값을 "open"으로 설정 | #### Send CLOVA Chatbot으로 이용자의 질문을 전달합니다. send 호출시, userid 값은 유일한 키값이면 아무거나 써도 됩니다. (예 회원번호 등) - userid : 유일한 키값 , description : 사용자 질의 | 구분 | Json Model - Example | | --- | --- | | Send | { "version": "v2", "userId": "U47b00b58c90f8e47428af8b7bddcda3d", "userIp": "8.8.8.8", "timestamp": 12345678, "bubbles": [ { "type": "text", "data" : { "description" : "text content which is user input" } } ], "event": "send" } | send 요청 필드 상세 설명 | 필드 이름 | 데이터 타입 | 필수여부 | 설명 | | --- | --- | --- | --- | | version | string | false | version은 'v2'가 기본이며, 값이 입력되지 않은 경우 'v1'으로 설정됨 | | userId | string | true | 봇과 채팅하는 사용자의 고유 ID로 최대 256자를 넘지 않아야 함 사용자마다 고유한 userId 설정 필요 | | userIp | string | false | 사용자의 Ip Adress (필수X) | | timestamp | long | true | - 타임스탬프 값 January 1, 1970, 00:00:00 GMT | | bubbles | array[[Text](/docs/ai-application-service-chatbot-chabot#Text)] | true | 하나의 Text Component 만 지원 하나 이상의 Text Component가 있을 경우, 마지막 Component를 사용자 요청을 사용함 | | event | string | true | 이벤트 value 값을 "send"으로 설정함 | #### getPersistentMenu 고정 메뉴 목록 가져오기 if need show persistent menu but local cache not exists, could request [PersistentMenu](/docs/ai-application-service-chatbot-chabot#PersistentMenu), will response persistentMenu field if fixed menu have set in messengers custom tab. 고정 메뉴 표시가 필요하지만 로컬 캐시가 존재하지 않으면 [PersistentMenu](/docs/ai-application-service-chatbot-chabot#PersistentMenu)를 요청할 수 있으며, 고정된 경우 persistMenu 필드에 응답합니다. 메뉴가 메신저 사용자 정의 탭에서 설정되었습니다. | 구분 | Json Model - Example | | --- | --- | | getPersistentMenu | { "version": "v2", "userId": "U47b00b58c90f8e47428af8b7bddcda3d", "userIp": "8.8.8.8", "timestamp": 12345678, "bubbles": [], "event": "getPersistentMenu" } | getPersistentMenu요청 필드 상세 설명 | 필드 이름 | 데이터 타입 | 필수여부 | 설명 | | --- | --- | --- | --- | | version | string | false | version은 'v2'가 기본이며, 값이 입력되지 않은 경우 'v1'으로 설정 | | userId | string | true | 봇과 채팅하는 사용자의 고유 ID로 최대 256자를 넘지 않아야 함 사용자마다 고유한 userId 설정 필요 | | userIp | string | false | 사용자의 Ip Adress (필수X) | | timestamp | long | true | - 타임스탬프 값 January 1, 1970, 00:00:00 GMT | | bubbles | array | true | 빈 Array 값으로 설정함 "[]" | | event | string | true | 이벤트 value 값을 "getPersistentMenu"으로 설정함 | ## 응답 ### 응답 결과 Status (성공 및 실패) #### Success - 챗봇 질의에 성공한 경우, Http Status Code는 200을 리턴하며, 응답 필드는 다음과 같습니다. | 구분 | Json Model - Example | | --- | --- | | Success | { "version": "v2", "userId": "U47b00b58c90f8e47428af8b7bddcda3d", "sessionId": "34a59946-5dcb-4b72-9b63-a773c659702e", "timestamp": 12345678, "bubbles": [ // each component is a bubble ], "quickButtons": [ // some buttons ], "scenario": { "name": "analyzedScenarioName", "intent": [ // some scenario intent ] }, "entities": [ { "word": "userInputWord", "name": "analyzedEntityName" } ], "keywords": [ { "keyword": "userInputKeyword", "group": "analyzedKeywordGroupName", "type": "analyzedKeywordType" } ], "persistentMenu": { // one template component }, "event": "send" } | 응답 결과(성공) 필드 상세 설명 | 필드 이름 | 데이터 타입 | 필수여부 | 설명 | | --- | --- | --- | --- | | version | string | false | version은 'v2'가 기본이며, 값이 입력되지 않은 요청의 경우 'v1'으로 응답됨 | | userId | string | true | 요청 시 설정한 userId 값과 동일함 | | sessionId | string | false | 현재 session id 이며, CLOVA Chatbot에서 관리하는 값 | | timestamp | long | true | 응답 타임스탬프 값 (milliseconds , January 1, 1970, 00:00:00 GMT) | | bubbles | array[[Component](/docs/ai-application-service-chatbot-chabot#Component)] | false | - 응답 컴포넌트의 배열 각 컴포넌트는 챗봇의 응답 Bubble과 매치됨 | | quickButtons | array[[Component](/docs/ai-application-service-chatbot-chabot#Component)] | false | 챗봇 하단에 설정된 고정버튼(quickButton)의 정보 | | scenario | jsonObject | false | - 사용자의 질의에 일치된 시나리오 분석 결과 시나리오 이름 및 의도(대화유형) 정보 제공 | | entities | array[jsonObject] | false | 사용자의 질의에 일치된 엔티티(entity)분석 결과 | | keywords | array[jsonObject] | false | 사용자 채팅에서 키워드와 일치하는 단어 키워드에는 "exactMatch"또는 "contain", "exactMatch"의 두 가지 유형 존재 "exactMatch"는 사용자 입력이 키워드와 완전히 일치 함을 의미하고 "contain"은 사용자 입력에 키워드가 포함됨을 의미 | | persistentMenu | [Template Component](/docs/ai-application-service-chatbot-chabot#TemplateComponent) | false | persistent menu를 설정함 [PersistentMenu](/docs/ai-application-service-chatbot-chabot#PersistentMenu) | | event | string | true | 고정된 응답 "send" | ![chatbot-03-clip_image002](https://cdn.document360.io/75897f19-b337-44ff-a363-411763705bdd/Images/Documentation/chatbot-03-clip_image002.png?sv=2026-02-06&spr=https&st=2026-11-07T17%3A10%3A17Z&se=2026-11-07T18%3A10%3A17Z&sr=c&sp=r&sig=jQeXN3hCsNSR6pszFOF75Aem9enicSpfZhU66DUryuI%3D) #### Error - 챗봇 질의에 실패한 경우, Http Status Code는 500을 리턴하며, 응답 필드는 다음과 같습니다. 500 오류 외 자세한 오류 코드는 문서 하단에 자세히 설명합니다. | HttpStatusCode | Description | | --- | --- | | 500 | Internal server error | Error Response Body: ``` { "code": "500", "message": "Internal server error", "timestamp": 12345678 } ``` ## Component 챗봇 답변의 응답 컴포넌트는 아래와 같은 json 구조를 따릅니다. ``` { "type": "...", "title": "optional, short bold text", "subTitle": "optional, short gray text", "data" : { ... } } ``` 가지 형태의 컴포넌트가 있습니다. - Basic Component - Composite Component - Flex Component ### Basic Component Basic 컴포넌트는 Text, Image, Button의 3가지의 기본 컴포넌트를 제공합니다. #### Text 챗봇의 답변할 Text 컴포넌트 형태이며, 타이틀, 서브 타이틀 그리고, 자세한 설명과 URL을 설정할 수 있습니다. - Text 컴포넌트 구조 ![chatbot-03-clip_image003](https://cdn.document360.io/75897f19-b337-44ff-a363-411763705bdd/Images/Documentation/chatbot-03-clip_image003.png?sv=2026-02-06&spr=https&st=2026-11-07T17%3A10%3A17Z&se=2026-11-07T18%3A10%3A17Z&sr=c&sp=r&sig=jQeXN3hCsNSR6pszFOF75Aem9enicSpfZhU66DUryuI%3D) - Text 컴포넌트 Json | 구분 | Json Model - Example | | --- | --- | | Text | { "type": "text", "title": "optional, short bold text", "subTitle": "optional, short gray text","data" : {"description" : "optional, a long text content","url" : "optional, a hyperlink at the bottom of description","urlAlias" :"optional, hyperlink show this alias","action": {Action Data} }} | - Text 컴포넌트 상세 설명 | 필드 이름 | 데이터 타입 | 필수 여부 | 설명 | | --- | --- | --- | --- | | type | string | true | text | | title | string | false | short bold text | | subTitle | string | false | short gray text | | data.description | string | false | a long text content | | data.url | string | false | the hyperlink jump url | | data.urlAlias | string | false | the hyperlink show text | | data.action | [Action](/docs/ai-application-service-chatbot-chabot#Action) | false | the action of click on text or title | #### Image Image를 포함한 답변 컴포넌트 형태이며, 이미지와 함께 타이틀, 서브 타이틀 그리고, 자세한 설명과 URL을 설정할 수 있습니다. - Image 컴포넌트 구조 ![chatbot-03-clip_image004](https://cdn.document360.io/75897f19-b337-44ff-a363-411763705bdd/Images/Documentation/chatbot-03-clip_image004.png?sv=2026-02-06&spr=https&st=2026-11-07T17%3A10%3A17Z&se=2026-11-07T18%3A10%3A17Z&sr=c&sp=r&sig=jQeXN3hCsNSR6pszFOF75Aem9enicSpfZhU66DUryuI%3D) - Image 컴포넌트 Json | 구분 | Json Model - Example | | --- | --- | | Image | { "type": "image", "title": "optional, short bold text", "subTitle": "optional, short gray text", "data" : { "imageUrl" : "https://ssl.pstatic.net/CloudFunctions.png", "alt" : "optional, short hint show when hover on image", "imagePosition" : "top", "description" : "optional, details info of image", "url" : "optional, a hyperlink at the bottom of description", "urlAlias" : "optional, hyperlink show this alias", "action": {[Action Data](/docs/ai-application-service-chatbot-chabot#Action)} } | - Image 컴포넌트 상세 설명 | 필드 이름 | 데이터 타입 | 필수 여부 | 설명 | | --- | --- | --- | --- | | type | string | true | image | | title | string | false | short bold text | | subTitle | string | false | short gray text | | data.imageUrl | string | true | image url, must be https url | | data.alt | string | false | short hint text show hover on image | | data.imagePosition | string | false | top / bottom / left / right, default is top | | data.description | string | false | details info of image | | data.url | string | false | the hyperlink jump url | | data.urlAlias | string | false | the hyperlink show text | | data.action | [Action](/docs/ai-application-service-chatbot-chabot#Action) | false | the action of click on image or title | #### Button Image를 포함한 답변 컴포넌트 형태이며, 이미지와 함께 타이틀, 서브 타이틀 그리고, 자세한 설명과 URL을 설정할 수 있습니다. - Button 컴포넌트 구조 ![chatbot-03-clip_image005](https://cdn.document360.io/75897f19-b337-44ff-a363-411763705bdd/Images/Documentation/chatbot-03-clip_image005.png?sv=2026-02-06&spr=https&st=2026-11-07T17%3A10%3A17Z&se=2026-11-07T18%3A10%3A17Z&sr=c&sp=r&sig=jQeXN3hCsNSR6pszFOF75Aem9enicSpfZhU66DUryuI%3D) - Button 컴포넌트 Json | 구분 | Json Model - Example | | --- | --- | | Button (Basic Button) | { "type": "button", "title": "optional, text show on button", "subTitle": "optional, short gray text", "data" : { "type": "basic", "iconUrl" : "https://ssl.pstatic.net/CloudFunctions.png", "action": {[Action Data](/docs/ai-application-service-chatbot-chabot#Action)} } } | | Button (Image Button) | { "type": "button", "title": "optional, text show on button", "subTitle": "optional, short gray text", "data" : { "type": "imageButton", "iconUrl" : "https://ssl.pstatic.net/CloudFunctions.png", "action": {[Action Data](/docs/ai-application-service-chatbot-chabot#Action)} } } | - Button 컴포넌트 상세 설명 | 필드 이름 | 데이터 타입 | 필수 여부 | 설명 | | --- | --- | --- | --- | | type | string | true | button | | title | string | false | text show on button | | subTitle | string | false | short gray text | | data.type | string | true | basic or imageButton | | data.iconUrl | string | false | button icon url, must be https url | | data.action | [Action](/docs/ai-application-service-chatbot-chabot#Action) | true | the action of click on button | ### Composite Component composite component 는 Template과 Carousel 컴포넌트로 구성됩니다. 각 특징에 맞게 챗봇의 응답을 챗봇 빌더에서 설정할 수 있으며, 챗봇 빌더를 통해 설정한 응답 결과가 아래의 json 형태로 응답됩니다. #### Template Component 템플릿은 기본 구성 요소로 구성됩니다. 템플릿에는 cover, contentTable, footTable의 세 부분이 있습니다. cover는 주요 내용입니다. contentTable, footTable은 테이블 레이아웃입니다. - Template Component 구조 ![chatbot-03-clip_image006](https://cdn.document360.io/75897f19-b337-44ff-a363-411763705bdd/Images/Documentation/chatbot-03-clip_image006.png?sv=2026-02-06&spr=https&st=2026-11-07T17%3A10%3A17Z&se=2026-11-07T18%3A10%3A17Z&sr=c&sp=r&sig=jQeXN3hCsNSR6pszFOF75Aem9enicSpfZhU66DUryuI%3D) - Template Component의 Json | 구분 | Json Model - Example | | --- | --- | | Template | { "type": "template", "title": "optional, short bold text", "subTitle": "optional, short gray text", "data":{ "cover":{ // any basic component }, "contentTableShowRows": 3, // if row count more than 3, should be fold "contentBackgroundImage":"https://ssl.pstatic.net/CloudFunctions.png", // optinal "contentTable":[ // table layout [ // first row { "colSpan": 1, "rowSpan": 2, "data":{ // any basic component type } }, // other cells in first row ], // another rows ], "footTableShowRows":3, // if row count more than 3, should be fold "footBackgroundImage":"https://ssl.pstatic.net/CloudFunctions.png", // optinal "footTable":[ // table layout same as contentTable ] } } | - Template Comonent 상세 설명 | 필드 이름 | 데이터 타입 | 필수 여부 | 설명 | | --- | --- | --- | --- | | type | string | true | template | | title | string | false | short bold text | | data.contentBackgroundImage | string | false | 컨텐츠 테이블 영역에 배경 이미지 표시 | | data.contentTable | array[][Cell] | false | 테이블 레이아웃, 2 차원 셀 배열이며, 셀 데이터는 기본 구성 요소 | | data.footTableShowRows | integer | false | 최대 행 수를 설정함 행 수가 최대 값보다 많으면 최대 행을 표시하고 접어야 하며 확장 버튼을 추가해야 함 설정하지 않으면 모든 행이 표시됨 | | data.footBackgroundImage | string | false | foot 테이블 영역에 배경 이미지 표시 | | data.footTable | array[][Cell] | false | contentTable과 동일하지만 contentTable이 지원할 수없는 경우가 아니면 항상 존재하지 않음 | - table layout의 상세 설명 | 필드 이름 | 데이터 타입 | 필수 여부 | 설명 | | --- | --- | --- | --- | | rowSpan | integer | true | span row count | | colSpan | integer | true | span column count | | data | [Basic Component](/docs/ai-application-service-chatbot-chabot#BasicComponent) | true | Text / Image / Button | #### Carousel Component 캐로셀 답변을 설정할 수 있습니다. ![chatbot-03-clip_image007](https://cdn.document360.io/75897f19-b337-44ff-a363-411763705bdd/Images/Documentation/chatbot-03-clip_image007.png?sv=2026-02-06&spr=https&st=2026-11-07T17%3A10%3A17Z&se=2026-11-07T18%3A10%3A17Z&sr=c&sp=r&sig=jQeXN3hCsNSR6pszFOF75Aem9enicSpfZhU66DUryuI%3D) - Carousel Component의 Json | 구분 | Json Model - Example | | --- | --- | | Carousel | { "type": "carousel", "title": "optional, short bold text", "subTitle": "optional, short gray text", "data" : { "cards": [ { // any component except carousel self and flex }, // more components ] } } | - Carousel Component 상세 설명 | 필드 이름 | 데이터 타입 | 필수 여부 | 설명 | | --- | --- | --- | --- | | type | string | true | carousel | ### Flex Component 모든 json 객체 형식을 지원합니다. 필요한 자체 JSON 사양을 정의 할 수 있습니다. 예 : [FlexMessageContainerObject](https://developers.line.biz/en/reference/messaging-api/#container) json을 사용할 수 있습니다. - Json in Flex | assortment | Json Model - Example | | --- | --- | | Flex | { "type": "flex", "title": "not used", "subTitle": "required, alternative text", "data" : { // any json object } } | - Flex Component Details | Field names | Data type | Required | Explanation | | --- | --- | --- | --- | | type | string | true | flex | | title | string | true | alternative text, show in chat list and push alert | | subTitle | string | false | not used | | data | json object | true | any json object. Example: line flex messsage could copy json from [Flex Message Simulator](https://developers.line.biz/console/fx/) | #### Special Messenger Component 1. LineFlex Line Messenger에서 지원하는 LineFlex 기능은 공통 Flex Component 형식으로 변경되었으며, 스펙은 동일합니다. 2. LineSticker 라인 메신저에서 지원하는 LineSticker기능은 sticker list의 형식을 따라서 스티커 메시지를 설정할 수 있습니다. - LineSticker의 Json | 구분 | Json Model - Example | | --- | --- | | LineSticker | { "type": "line_sticker", "data" : { "packageId": "packageId of LINE", "stickerId": "stickerId of LINE" } } | - LineSticker 컴포넌트 상세 설명 | 필드 이름 | 데이터 타입 | 필수 여부 | 설명 | | --- | --- | --- | --- | | type | string | true | line_sticker | | data.packageId | string | true | sticker's packageId of LINE, refer to [sticker list](https://developers.line.biz/media/messaging-api/sticker_list.pdf) | | data.stickerId | string | true | sticker's stickerId of LINE, refer to [sticker list](https://developers.line.biz/media/messaging-api/sticker_list.pdf) | 1. LineWorksSticker 라인 웍스에서 지원하는 LineWorksSticker 기능은 sticker list의 형식을 따라서 스티커 메시지를 설정할 수 있습니다. - LineWorksSticker의 Json | 구분 | Json Model - Example | | --- | --- | | LineWorksSticker | { "type": "lineworks_sticker", "data" : { "packageId": "packageId of LINEWORKS", "stickerId": "stickerId of LINEWORKS" } } | - LineWorksSticker 컴포넌트 상세 설명 | 필드 이름 | 데이터 타입 | 필수 여부 | 설명 | | --- | --- | --- | --- | | type | string | true | lineworks_sticker | | data.packageId | string | true | sticker's packageId of LINEWORKS, refer to [sticker list](https://static.worksmobile.net/static/wm/media/message-bot-api/line_works_sticker_list_new.pdf) | | data.stickerId | string | true | sticker's stickerId of LINEWORKS, refer to [sticker list](https://static.worksmobile.net/static/wm/media/message-bot-api/line_works_sticker_list_new.pdf) | #### Action Action은 모든 구성요소에 대한 공통 데이터입니다. Component를 클릭했을 때 수행할 작업을 정의합니다. - **Postback** 컴포넌트를 클릭하면 postbackText를 챗봇에 포스트 백하고 포스트 백을 사용자 채팅으로 표시합니다. - Postback의 Json | 구분 | Json Model - Example | | --- | --- | | Postback | { "type": "line_sticker", "data" : { "packageId": "packageId of LINE", "stickerId": "stickerId of LINE" } } | - Postback 컴포넌트 상세 설명 | 필드 이름 | 데이터 타입 | 필수 여부 | 설명 | | --- | --- | --- | --- | | type | string | true | postback | | data.postback | string | true | text show as user chat, if send this field to chatbot, not affect previous features, but will not support some new features | | data.postbackFull | string | true | postback full content send to chatbot | - **Utterance** 구성 요소를 클릭하면 텍스트를 챗봇에 포스트 백(Postback)하고 텍스트를 사용자 채팅으로 표시합니다. - Utterance의 Json | 구분 | Json Model - Example | | --- | --- | | Utterance | { "type": "utterance", "data" : { "utteranceId" : 1, "text" : "text show in chat window", "postback" : "postback text" } } | - Utterance Component 상세 설명 | 필드 이름 | 데이터 타입 | 필수 여부 | 설명 | | --- | --- | --- | --- | | type | string | true | utterance | | data.utteranceId | string | true | | | data.text | string | true | text show in chat window as user input | | data.postback | string | true | postback text send to chatbot | - **Link** 컴포넌트를 클릭하면 URL로 이동합니다. - Link의 Json | 구분 | Json Model - Example | | --- | --- | | Link | { "type": "link", "data" : { "url" : "http://www.ncloud.com", "mobileUrl" : "http://m.ncloud.com" } } | - Link Component 상세 설명 | 필드 이름 | 데이터 타입 | 필수 여부 | 설명 | | --- | --- | --- | --- | | type | string | true | postback | | data.url | string | true | open url | | data.mobileUrl | string | false | url for mobile device | - **Phone** 컴포넌트를 클릭하면 다이얼 페이지로 이동합니다. 모바일에서만 지원됩니다. - Phone Component의 Json | 구분 | Json Model - Example | | --- | --- | | Phone | { "type": "phone", "data" : { "number" : "400-1111-1111", "name" : "Customer service" } } | - Phone Component 상세 설명 | 필드 이름 | 데이터 타입 | 필수 여부 | 설명 | | --- | --- | --- | --- | | type | string | true | phone | | data.number | string | true | phone number | | data.name | string | false | contact name | - **Welcome** 컴포넌트를 클릭하면 open event가 전송됩니다. - Welcome Component의 Json | 구분 | Json Model - Example | | --- | --- | | Welcome | { "type": "welcome", "data" : { "postback" : "postback text, optional" } } | - Welcome Component 상세 설명 | 필드 이름 | 데이터 타입 | 필수 여부 | 설명 | | --- | --- | --- | --- | | type | string | true | welcome | | data.postback | string | false | 포스트 백 텍스트가 열린 이벤트에서 챗봇으로 전송 | #### Quick Button 채팅 창 하단에있는 그룹 고정 버튼입니다. #### Persistent Menu 고정 메뉴는 사용자가 채팅 바에서 메뉴 버튼을 터치하면 표시됩니다. 항상 환영 응답에 포함됩니다. utils를 변경하지 않음 다른 응답에 permanentMenu 컨텐츠가 포함되어 있습니다. ![chatbot-03-clip_image023_ko](https://cdn.document360.io/75897f19-b337-44ff-a363-411763705bdd/Images/Documentation/chatbot-03-clip_image023.png?sv=2026-02-06&spr=https&st=2026-11-07T17%3A10%3A17Z&se=2026-11-07T18%3A10%3A17Z&sr=c&sp=r&sig=jQeXN3hCsNSR6pszFOF75Aem9enicSpfZhU66DUryuI%3D) Persistent menu is a Template Component, with these definition: - title will show on the chat bar. - no cover, cover should be discard. - no foot table, fields relate to foot area should be discard. - contentBackgroundImage is the background, if the components in the contentTable have image, will cover the background. ## 오류 코드 오류 응답에 대해서 설명합니다. | HttpStatusCode | Description | | --- | --- | | 500 | Internal server error | - Error Response Body: ``` { "code": "1001", "message": "domain code test not found", "timestamp": 12345678 } ``` - Body Introduce | Field | Type | Must Exists | Description | | --- | --- | --- | --- | | code | string | true | error code | | event | string | true | fixed string value "send" | | timestamp | long | true | response time milliseconds since January 1, 1970, 00:00:00 GMT | 오류 코드 | ErrorCode | Description | | --- | --- | | 4000 | request param invalid | | 4010 | Unauthorized | | 4030 | Forbidden to access | | 4031 | Signature validate failed | | 4032 | timestamp exceeded time window(10000ms) | | 1000 | version not support | | 1001 | Not found domain code | | 1002 | check url param is invalid | | 5000 | Unknown service error | | 5010 | Current protocol version not support this reply structure | | 5020 | Calls to this api have exceeded the rate limit | ## API 예제 다음은 각 언어별 CLOVA Custom API 구현 예제입니다 - JAVA - Chatbot Custom API call source ``` package com.ncp.ai.demo.process; import android.media.MediaPlayer; import android.os.Environment; import android.util.Base64; import java.io.BufferedReader; import java.io.DataOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLEncoder; import java.security.Timestamp; import java.util.Date; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import android.util.Base64; import org.json.JSONArray; import org.json.JSONObject; public class ChatbotProc { public static String main(String voiceMessage, String apiURL, String secretKey) { String chatbotMessage = ""; try { //String apiURL = "https://ex9av8bv0e.apigw.ntruss.com/custom_chatbot/prod/"; URL url = new URL(apiURL); String message = getReqMessage(voiceMessage); System.out.println("##" + message); String encodeBase64String = makeSignature(message, secretKey); HttpURLConnection con = (HttpURLConnection)url.openConnection(); con.setRequestMethod("POST"); con.setRequestProperty("Content-Type", "application/json;UTF-8"); con.setRequestProperty("X-NCP-CHATBOT_SIGNATURE", encodeBase64String); // post request con.setDoOutput(true); DataOutputStream wr = new DataOutputStream(con.getOutputStream()); wr.write(message.getBytes("UTF-8")); wr.flush(); wr.close(); int responseCode = con.getResponseCode(); BufferedReader br; if(responseCode==200) { // Normal call System.out.println(con.getResponseMessage()); BufferedReader in = new BufferedReader( new InputStreamReader( con.getInputStream())); String decodedString; while ((decodedString = in.readLine()) != null) { chatbotMessage = decodedString; } //chatbotMessage = decodedString; in.close(); } else { // Error occurred chatbotMessage = con.getResponseMessage(); } } catch (Exception e) { System.out.println(e); } return chatbotMessage; } public static String makeSignature(String message, String secretKey) { String encodeBase64String = ""; try { byte[] secrete_key_bytes = secretKey.getBytes("UTF-8"); SecretKeySpec signingKey = new SecretKeySpec(secrete_key_bytes, "HmacSHA256"); Mac mac = Mac.getInstance("HmacSHA256"); mac.init(signingKey); byte[] rawHmac = mac.doFinal(message.getBytes("UTF-8")); encodeBase64String = Base64.encodeToString(rawHmac, Base64.NO_WRAP); return encodeBase64String; } catch (Exception e){ System.out.println(e); } return encodeBase64String; } public static String getReqMessage(String voiceMessage) { String requestBody = ""; try { JSONObject obj = new JSONObject(); long timestamp = new Date().getTime(); System.out.println("##"+timestamp); obj.put("version", "v2"); obj.put("userId", "U47b00b58c90f8e47428af8b7bddc1231heo2"); //=> userId is a unique code for each chat user, not a fixed value, recommend use UUID. use different id for each user could help you to split chat history for users. obj.put("timestamp", timestamp); JSONObject bubbles_obj = new JSONObject(); bubbles_obj.put("type", "text"); JSONObject data_obj = new JSONObject(); data_obj.put("description", voiceMessage); bubbles_obj.put("type", "text"); bubbles_obj.put("data", data_obj); JSONArray bubbles_array = new JSONArray(); bubbles_array.put(bubbles_obj); obj.put("bubbles", bubbles_array); obj.put("event", "send"); requestBody = obj.toString(); } catch (Exception e){ System.out.println("## Exception : " + e); } return requestBody; } } ``` - Example of calling ChatbotProc.java above in UI stage ``` package com.example.user.ncpaidemo; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.AsyncTask; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.Message; import android.speech.tts.Voice; import android.util.Log; import android.view.MenuItem; import android.view.View; import android.widget.Button; import android.widget.TextView; import com.naver.speech.clientapi.SpeechRecognitionResult; import com.ncp.ai.demo.process.ChatbotProc; import com.ncp.ai.demo.process.CsrProc; import com.ncp.ai.demo.process.CssProc; import com.ncp.ai.utils.AudioWriterPCM; import org.json.JSONArray; import org.json.JSONObject; import java.lang.ref.WeakReference; import java.util.List; public class VoiceChatbotActivity extends BaseActivity { private static final String TAG = VoiceChatbotActivity.class.getSimpleName(); private RecognitionHandler handler; private CsrProc naverRecognizer; private TextView txtResult; private Button btnStart; private String mResult; private AudioWriterPCM writer; private String clientId; private String clientSecret; // Handle speech recognition Messages. private void handleMessage(Message msg) { switch (msg.what) { case R.id.clientReady: // Voice recognition ready txtResult.setText("Connected"); writer = new AudioWriterPCM(Environment.getExternalStorageDirectory().getAbsolutePath() + "/NaverSpeechTest"); writer.open("Test"); break; case R.id.audioRecording: writer.write((short[]) msg.obj); break; case R.id.partialResult: mResult = (String) (msg.obj); mResult += mResult; txtResult.setText(mResult); break; case R.id.finalResult: // Final recognition result SpeechRecognitionResult speechRecognitionResult1 = (SpeechRecognitionResult) msg.obj; List results1 = speechRecognitionResult1.getResults(); StringBuilder strBuf1 = new StringBuilder(); for(String result : results1) { strBuf1.append(result); //strBuf.append("\n"); break; } mResult = strBuf1.toString(); txtResult.setText(mResult); requestChatbot(); break; case R.id.recognitionError: if (writer != null) { writer.close(); } mResult = "Error code : " + msg.obj.toString(); txtResult.setText(mResult); btnStart.setText(R.string.str_start); btnStart.setEnabled(true); break; case R.id.clientInactive: if (writer != null) { writer.close(); } btnStart.setText(R.string.str_start); btnStart.setEnabled(true); break; } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_voice_chatbot); SharedPreferences sharedPref = getSharedPreferences("PREF", Context.MODE_PRIVATE); clientId = sharedPref.getString("application_client_id", ""); clientSecret = sharedPref.getString("application_client_secret", ""); txtResult = (TextView) findViewById(R.id.textViewVoiceChatbotResult); btnStart = (Button) findViewById(R.id.btn_voice_chatbot1); handler = new RecognitionHandler(this); //naverRecognizer = new CsrProc(this, handler, clientId); naverRecognizer = CsrProc.getCsrProc(this, clientId); naverRecognizer.setHandler(handler); btnStart.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (!naverRecognizer.getSpeechRecognizer().isRunning()) { mResult = ""; txtResult.setText("Connecting..."); btnStart.setText(R.string.str_stop); naverRecognizer.recognize(); } else { Log.d(TAG, "stop and wait Final Result"); btnStart.setEnabled(false); naverRecognizer.getSpeechRecognizer().stop(); } } }); Button voiceChatbotReplay; voiceChatbotReplay = (Button) findViewById(R.id.btn_voice_chatbot_replay); voiceChatbotReplay.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { TextView txtResult = (TextView) findViewById(R.id.text_voice_chatbot_replay); CSSExecute(txtResult.getText().toString()); } }); } private void requestChatbot() { SharedPreferences sharedPref = getSharedPreferences("PREF", Context.MODE_PRIVATE); String chatbotApiGwUrl = sharedPref.getString("chatbot_api_gw_url", ""); String chatbotSecretKey = sharedPref.getString("chatbot_secret_key", ""); TextView csrSourceText = (TextView)findViewById(R.id.textViewVoiceChatbotResult); String text = csrSourceText.getText().toString(); VoiceChatbotActivity.VoiceChatbotTask task = new VoiceChatbotActivity.VoiceChatbotTask(); task.execute(text, chatbotApiGwUrl, chatbotSecretKey); } @Override protected void onStart() { super.onStart(); // Voice recognition server initialization is here naverRecognizer.getSpeechRecognizer().initialize(); } @Override protected void onResume() { super.onResume(); mResult = ""; txtResult.setText(""); btnStart.setText(R.string.str_start); btnStart.setEnabled(true); } // @Override // protected void onStop() { // System.out.println("voice chatbot End!!!"); // super.onStop(); // terminate voice recognition server // naverRecognizer.getSpeechRecognizer().release(); // } // Declare handler for handling SpeechRecognizer thread's Messages. static class RecognitionHandler extends Handler { private final WeakReference mActivity; RecognitionHandler(VoiceChatbotActivity activity) { mActivity = new WeakReference(activity); } @Override public void handleMessage(Message msg) { VoiceChatbotActivity activity = mActivity.get(); if (activity != null) { activity.handleMessage(msg); } } } public class VoiceChatbotTask extends AsyncTask { @Override public String doInBackground(String... strings) { return ChatbotProc.main(strings[0], strings[1], strings[2]); } @Override protected void onPostExecute(String result) { ReturnThreadResult(result); } } public String ReturnThreadResult(String result) { //{"version":"v2","userId":"U47b00b58c90f8e47428af8b7bddc1231heo2","sessionId":"617666","timestamp":1546593912020, // "bubbles":[{"type":"template","data":{"cover":{"type":"text","data":{"description":"b"}},"contentTable":[[{"rowSpan":1,"colSpan":1,"data":{"type":"button","title":"b","data":{"type":"basic","action":{"type":"link","data":{"url":"https://www.ncloud.com/product"}}}}}],[{"rowSpan":1,"colSpan":1,"data":{"type":"button","title": "b","data":{"type":"basic","action":{"type":"link","data":{"url":"https://www.ncloud.com/product"}}}}}]]}}],"event":"send"} //{"version":"v2","userId":"U47b00b58c90f8e47428af8b7bddc1231heo2","sessionId":"641799","timestamp":1546777198124, // "bubbles":[{"type":"text","data":{"description":"b"}}],"event":"send"} String chatbotMessage = ""; String rlt = result; try{ JSONObject jsonObject = new JSONObject(rlt); JSONArray bubbles = jsonObject.getJSONArray("bubbles"); for (int i =0; i < bubbles.length(); i++){ JSONObject bubble = bubbles.getJSONObject(i); String chatType = bubble.getString("type"); if (chatType.equals("text")){ chatbotMessage = bubble.getJSONObject("data").getString("description"); }else if (chatType.equals("template")) { chatbotMessage = bubble.getJSONObject("data").getJSONObject("cover").getJSONObject("data").getString("description"); }else { chatbotMessage = ""; } TextView txtResult = (TextView) findViewById(R.id.text_voice_chatbot_replay); txtResult.setText(chatbotMessage); break; } } catch (Exception e) { System.out.println(e); } CSSExecute(chatbotMessage); return chatbotMessage; } private void CSSExecute(String message) { VoiceChatbotActivity.NaverTTSTask tts = new VoiceChatbotActivity.NaverTTSTask(); tts.execute(message, "mijin", clientId, clientSecret); } public class NaverTTSTask extends AsyncTask { @Override public String doInBackground(String... strings) { System.out.println(strings[1]); CssProc.main(strings[0], strings[1], strings[2], strings[3]); return null; } } } ``` - Python - CLOVA Chatbot Custom API v2 ``` import hashlib import hmac import base64 import time import requests import json class ChatbotMessageSender: # chatbot api gateway url ep_path = '' # chatbot custom secret key secret_key = '' def req_message_send(self): timestamp = self.get_timestamp() request_body = { 'version': 'v2', 'userId': 'U47b00b58c90f8e47428af8b7bddcda3d1111111', 'timestamp': timestamp, 'bubbles': [ { 'type': 'text', 'data': { 'description': 'About Me' } } ], 'event': 'send' } ## Request body encode_request_body = json.dumps(request_body).encode('UTF-8') ## make signature signature = self.make_signature(self.secret_key, encode_request_body) ## headers custom_headers = { 'Content-Type': 'application/json;UTF-8', 'X-NCP-CHATBOT_SIGNATURE': signature } print("## Timestamp : ", timestamp) print("## Signature : ", signature) print("## headers ", custom_headers) print("## Request Body : ", encode_request_body) ## POST Request response = requests.post(headers=custom_headers, url=self.ep_path, data=encode_request_body) return response @staticmethod def get_timestamp(): timestamp = int(time.time() * 1000) return timestamp @staticmethod def make_signature(secret_key, request_body): secret_key_bytes = bytes(secret_key, 'UTF-8') signing_key = base64.b64encode(hmac.new(secret_key_bytes, request_body, digestmod=hashlib.sha256).digest()) return signing_key if __name__ == '__main__': res = ChatbotMessageSender().req_message_send() print(res.status_code) if(res.status_code == 200): print(res.text) #print(res.read().decode("UTF-8")) ``` - php ``` function makeSignature($secretKey, $requestBody) { $signautue = base64_encode(hash_hmac('sha256', $requestBody, $secretKey,true)); //echo "this is signiture : ".$signautue."\n"; return $signautue; } try { // User IP if (isset($_SERVER['HTTP_X_FORWARDED_FOR']) && $_SERVER['HTTP_X_FORWARDED_FOR']) { $clientIpAddress = $_SERVER['HTTP_X_FORWARDED_FOR']; } else { $clientIpAddress = $_SERVER['HTTP_X_FORWARDED_FOR']; } $timestamp = ""; $microtime = ""; list($microtime,$timestamp) = explode(' ',microtime()); $timestamp = $timestamp.substr($microtime, 2, 3); $url = "https://xi3mfpym2x.apigw.ntruss.com/send/beta/"; $requestBody= '{ "version": "v2", "userId": "U47b00b58c90f8e47428af8b7bddcda3d231", "userIp": "'.$clientIpAddress.'", "timestamp": '.$timestamp.', "bubbles": [ { "type": "text", "data" : { "description" : "postback text of welcome action" } } ], "event": "open" }'; $secretKey = "YnVFTWZDemt3bUZhaEJlalN1Z3ZNY2pzeVp0aVRjd04=12"; $signautue = makeSignature($secretKey,$requestBody); $is_post = true; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST"); curl_setopt($ch, CURLOPT_POSTFIELDS, $requestBody); $headers = array(); $headers[] = "Content-Type:application/json; charset=utf-8"; $headers[] = "X-NCP-CHATBOT_SIGNATURE: " .$signautue; curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); $response = curl_exec($ch); $err = curl_error($ch); curl_close ($ch); if ($err) { echo "cURL Error #:" . $err; } else { echo $response; } } catch(Exception $E) { echo "Response: ". $E->lastResponse . "\n"; } ?> ``` - swift ``` import UIKit import CommonCrypto // chatbot custom secret key let secret_key = ""; // chatbot api gateway url let invoke_url = ""; class ViewController: UIViewController, URLSessionDelegate { override func viewDidLoad() { super.viewDidLoad() demo() } func demo() { let timeInterval: TimeInterval = NSDate().timeIntervalSince1970 let millisecond = CLongLong(round(timeInterval*1000)) let body = ["bubbles":[["data": ["description":"test"], "type":"text"]], "event":"send", "timestamp":millisecond, "userId":"test", "version":"v2"] as NSDictionary do { let jsonDataFromBody = try JSONSerialization.data(withJSONObject: body, options: JSONSerialization.WritingOptions.prettyPrinted) let jsonBytes = [CUnsignedChar](jsonDataFromBody) let jsonBytesPointer = UnsafePointer(jsonBytes) //UnsafeRawPointer let cKey = secret_key.cString(using: String.Encoding.utf8) let digestLen = Int(CC_SHA256_DIGEST_LENGTH) let sha256 = UnsafeMutablePointer.allocate(capacity: digestLen) CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA256), cKey, secret_key.lengthOfBytes(using: String.Encoding.utf8), jsonBytesPointer, jsonBytes.count, sha256) let base64_sha256 = Data.init(bytes: sha256, count: Int(CC_SHA256_DIGEST_LENGTH)).base64EncodedString() httpsRequest("X-NCP-CHATBOT_SIGNATURE", base64_sha256, body: jsonDataFromBody) sha256.deallocate() } catch { print(error) } } func httpsRequest(_ signHeaderKey:String, _ signature:String, body:Data) { let url = URL.init(string: invoke_url)! var request = URLRequest.init(url: url) request.addValue(signature, forHTTPHeaderField: signHeaderKey) request.addValue("application/json", forHTTPHeaderField: "Content-Type") request.httpMethod = "POST" request.httpBody = body let session = URLSession.shared let task = session.dataTask(with: request) { (data:Data?, response:URLResponse?, error:Error?) in print(String.init(data:data!, encoding: .utf8)!) } task.resume() } func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { if challenge.protectionSpace.authenticationMethod != NSURLAuthenticationMethodServerTrust { return } let credential = URLCredential.init(trust: challenge.protectionSpace.serverTrust!) completionHandler(URLSession.AuthChallengeDisposition.useCredential, credential) } } ``` - objective-c ``` #import "ViewController.h" #import #import // chatbot custom secret key static NSString *secret_key = @""; // chatbot api gateway url static NSString *invoke_url = @""; @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; [self demo]; } - (void)demo { NSTimeInterval time = [[NSDate date] timeIntervalSince1970]*1000; long long millisecond = [[NSNumber numberWithDouble:time] longLongValue]; NSDictionary *body = @{@"bubbles":@[@{@"data":@{@"description":@"test"}, @"type":@"text"}], @"event":@"send", @"timestamp":millisecond, @"userId":@"U47b00b58c90f8e47428af8b7bddcda3d1111111", @"version":@"v2" }; NSError *error; NSData *jsondataFrombody = [NSJSONSerialization dataWithJSONObject:body options:0 error:&error]; const char *cKey = [secret_key cStringUsingEncoding:NSUTF8StringEncoding]; NSMutableData* sha256 = [NSMutableData dataWithLength:CC_SHA256_DIGEST_LENGTH]; CCHmac(kCCHmacAlgSHA256, cKey, secret_key.length, jsondataFrombody.bytes, jsondataFrombody.length, sha256.mutableBytes); NSString *base64_sha256 = [sha256 base64EncodedStringWithOptions:0]; [self httpsRequest:@"X-NCP-CHATBOT_SIGNATURE" signature:base64_sha256 bodyData:jsondataFrombody]; } - (void)httpsRequest:(NSString *)signHeaderKey signature:(NSString *)sign bodyData:(NSData *)bodyData { NSURL *url = [NSURL URLWithString:invoke_url]; NSMutableURLRequest *mRequest = [NSMutableURLRequest requestWithURL:url]; [mRequest setValue:sign forHTTPHeaderField:signHeaderKey]; [mRequest setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; mRequest.HTTPMethod = @"POST"; mRequest.HTTPBody = bodyData; NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];//replace delegateQueue with your workingQueue NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:mRequest completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { NSLog(@"response: %@", [response description]); NSLog(@"response data:%@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]); }]; [dataTask resume]; } - (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler { if (![challenge.protectionSpace.authenticationMethod isEqualToString:@"NSURLAuthenticationMethodServerTrust"]) { return; } NSURLCredential *credential = [[NSURLCredential alloc] initWithTrust:challenge.protectionSpace.serverTrust]; completionHandler(NSURLSessionAuthChallengeUseCredential,credential); } @end ```