메시징 쓰기
상담 콘솔이 대화를 운영하는 데 쓰는 쓰기 엔드포인트입니다. 답장 전송, 읽음 처리, 담당자 배정, 채널 종료·재개, 상담 태그 부여·해제, 상담 메모 쓰기를 제공합니다. 읽기 엔드포인트와 같은 Base URL, 응답 봉투, 에러 규약을 따릅니다.
쓰기 엔드포인트는 BFF(서버 사이드)에서만 호출하세요. Public API는 CORS를 허용하지 않으며, PAT는 워크스페이스 전체를 읽고 쓸 수 있는 비밀 값입니다.
답장 전송
POST /channels/{id}/messages — 채널에 메시지를 전송합니다.
- Scope:
messages:write - Body
| 필드 | 타입 | 설명 |
|---|---|---|
content_type | text | markdown | 본문 형식 |
content | string | 메시지 본문 (1–2000자, 초과 시 400 INVALID_BODY) |
sender_member_id | string · 선택 | 발신 상담원 membership ID. 생략하면 토큰의 integration 신원으로 귀속됩니다 (하이브리드 귀속, 아래 참고) |
client_request_id | string · 선택 | 멱등성 키 (최대 128자). 같은 값으로 재전송하면 원본 메시지를 반환합니다 |
아래 요청은 상담원 m1n2o3-...의 이름으로 답장을 한 건 보냅니다.
curl -X POST "https://api.talk.zeroworks.ai/api/public/v1/channels/c1d2.../messages" \
-H "Authorization: Bearer ztpat_..." \
-H "Content-Type: application/json" \
-d '{
"content_type": "text",
"content": "안녕하세요, 무엇을 도와드릴까요?",
"sender_member_id": "m1n2o3-...",
"client_request_id": "req-7f3a..."
}'
응답 — 신규 생성 시 201, 멱등 중복 시 200(deduplicated: true)입니다. 아래는 sender_member_id를 생략해 토큰 신원으로 보낸 경우의 응답입니다.
{
"success": true,
"data": {
"id": "msg-9z8y-...",
"channel_id": "c1d2e3f4-...",
"sender_id": "4d5e6f7a-...",
"sender_type": "integration",
"sender_name": "API",
"content_type": "text",
"content": "안녕하세요, 무엇을 도와드릴까요?",
"created_at": "2026-06-02T01:23:45Z",
"updated_at": "2026-06-02T01:23:45Z",
"client_request_id": "req-7f3a...",
"deduplicated": false
}
}
하이브리드 귀속
sender_member_id를 지정하면 해당 상담원으로 귀속됩니다. 그 상담원은message:write권한이 있어야 합니다. 호출 측은 자신의 상담원을 ZeroTalk membership ID에 매핑해야 합니다.sender_member_id를 생략하면 토큰의 integration 신원으로 귀속됩니다. 메시지의sender_type은integration,sender_id는 토큰 ID이고, 표시 이름(sender_name)은 대시보드 설정 → 개발자에서 그 토큰의 「발신 신원 수정」으로 지정한 이름입니다. 지정하지 않았으면API입니다. 토큰 prefix는 이름으로 쓰지 않습니다. 상담원 측 렌더링은 이벤트 문서의sender_type을 참고하세요.
에코와 멱등성
- 전송한 답장은
message.created웹훅으로 되돌아옵니다. 웹훅에는client_request_id가 없으므로, **응답의data.id**를 웹훅의data.message_id와 대조해 자신의 전송을 식별하세요. client_request_id는 전송 멱등성 용도입니다. 실패한POST를 같은 키로 재시도하면 새 메시지를 만들지 않고 원본을200(deduplicated: true)으로 반환합니다. 웹훅 상관(correlation) 키로는 쓰지 마세요.
답장 전송 오류
| 코드 | 발생 조건 | 대응 |
|---|---|---|
400 INVALID_BODY | JSON 형식 오류, content_type이 text·markdown이 아님, content가 1–2000자를 벗어남 | 본문을 고쳐 다시 보냅니다 |
403 FORBIDDEN | 지정한 상담원에게 message:write 권한이 없음 (예: 뷰어) | 권한이 있는 상담원을 지정하거나 sender_member_id를 생략합니다 |
404 NOT_FOUND | 채널이 이 워크스페이스에 없음 | 채널 ID를 확인합니다 |
422 INVALID_SENDER_MEMBER | sender_member_id가 UUID가 아니거나 활성 멤버가 아님 | 멤버 조회로 ID를 확인합니다 |
채널 조작의 행위자
종료·재개·배정·태그 부여/해제·상담 메모 쓰기에서 actor_member_id는 선택입니다. 답장의 sender_member_id와 같은 규칙입니다. 생략하면 토큰 자신이, 지정하면 그 상담원이 한 일이 됩니다. 단, 상담 메모 쓰기에서는 지정한 상담원을 권한·범위 판정에만 쓰고 시스템 메시지·감사 기록에 남기지 않으므로 아래 표가 해당하지 않습니다(상담 메모 쓰기의 권한 참고).
| 누가 한 일로 남나 | 필요한 권한 | |
|---|---|---|
생략 (본문 없음·null·"" 포함) | 토큰. 상담 화면의 시스템 메시지에 토큰의 표시 이름(없으면 API)과 (자동화)가 붙고, 감사 로그의 주체에 토큰 이름이 남습니다 | channels:write scope 만 |
| 지정 | 그 상담원. 시스템 메시지에 상담원 이름과 (자동화)가 붙고, 감사 로그의 주체는 토큰 이름에 "○○ 대행"이 함께 남습니다 | 그 상담원에게 channel:operate 권한(없으면 403 FORBIDDEN)과 그 상담에 대한 접근 범위(없으면 404) |
읽음 처리만 예외로 actor_member_id가 필수입니다. 읽음은 특정 상담원의 읽음 위치를 옮기는 일이라 토큰에는 해당하지 않습니다(누락 시 400 INVALID_BODY).
actor_member_id가 UUID가 아니거나 이 워크스페이스의 멤버가 아니면 422 INVALID_ACTOR_MEMBER를 반환합니다. 상담원의 권한 정책을 읽지 못하면 503 PERMISSION_RESOLVE_FAILED를 반환하며, 이 경우 잠시 뒤 다시 요청하세요.
읽음 처리
POST /channels/{id}/read — 채널을 특정 메시지까지 읽음 처리합니다.
- Scope:
channels:write - Body:
{ "actor_member_id": "...", "message_id": "..." }—actor_member_id필수,message_id필수 - 응답:
204 No Content
아래 요청은 상담원 m1n2o3-...의 읽음 위치를 메시지 msg-9z8y-...까지 옮깁니다.
curl -X POST "https://api.talk.zeroworks.ai/api/public/v1/channels/c1d2.../read" \
-H "Authorization: Bearer ztpat_..." \
-H "Content-Type: application/json" \
-d '{ "actor_member_id": "m1n2o3-...", "message_id": "msg-9z8y-..." }'
종료 / 재개
POST /channels/{id}/close · POST /channels/{id}/reopen — 채널을 종료하거나 재개합니다.
- Scope:
channels:write(행위자를 지정하면 그 상담원에게channel:operate권한 필요 — 없으면403 FORBIDDEN) - Body: 비워 두거나
{ "actor_member_id": "..." } - 응답:
200,data는 갱신된 채널 객체(채널 조회와 동일 스키마)입니다. 변경된status를 별도 GET 없이 확인할 수 있습니다.
아래 요청은 토큰 자신의 이름으로 채널을 종료하고, 이어서 상담원 m1n2o3-...의 이름으로 종료합니다.
# 토큰 자신이 종료
curl -X POST "https://api.talk.zeroworks.ai/api/public/v1/channels/c1d2.../close" \
-H "Authorization: Bearer ztpat_..."
# 상담원 대신 종료
curl -X POST "https://api.talk.zeroworks.ai/api/public/v1/channels/c1d2.../close" \
-H "Authorization: Bearer ztpat_..." \
-H "Content-Type: application/json" \
-d '{ "actor_member_id": "m1n2o3-..." }'
종료와 재개는 각각 channel.closed·channel.reopened 웹훅을 발생시킵니다.
담당자 배정
POST /channels/{id}/assign — 채널 담당자를 지정하거나 해제합니다.
- Scope:
channels:write(행위자를 지정하면 그 상담원에게channel:operate권한 필요) - Body:
{ "member_id": "...", "actor_member_id": "..." }—actor_member_id는 선택입니다.member_id를null로 보내면 배정이 해제됩니다. - 응답:
200,data는 갱신된 채널 객체(채널 조회와 동일 스키마)입니다. 변경된assigned_member_id를 별도 GET 없이 확인할 수 있습니다.
아래 요청은 상담원 m1n2o3-...의 이름으로 채널 담당자를 p4q5r6-...로 지정합니다.
curl -X POST "https://api.talk.zeroworks.ai/api/public/v1/channels/c1d2.../assign" \
-H "Authorization: Bearer ztpat_..." \
-H "Content-Type: application/json" \
-d '{ "actor_member_id": "m1n2o3-...", "member_id": "p4q5r6-..." }'
배정 변경은 channel.assigned 웹훅을 발생시킵니다. 현재 담당자는 채널 조회의 assigned_member_id와 웹훅 envelope의 assigned_member 스냅샷에서 확인합니다.
상담시간 밖이라 자동 이관이 거부되면 409 HANDOFF_OUTSIDE_BUSINESS_HOURS를 반환합니다. 같은 요청을 다시 보내도 같은 응답이 나옵니다.
태그 부여·해제
상담(채널)에 태그를 붙이고 뗍니다. 붙어 있는 태그는 채널 객체의 tags 배열에서 확인하고, 워크스페이스에 어떤 태그가 있는지는 상담 태그에서 조회합니다.
부여
POST /channels/{id}/tags
- Scope:
channels:write(행위자를 지정하면 그 상담원에게channel:operate권한 필요) - Body:
{ "tags": ["환불요청"], "tag_ids": ["t1a2..."], "actor_member_id": "..." }—actor_member_id는 선택입니다.tags(이름)와tag_ids(사전 id) 중 최소 하나는 있어야 합니다. 둘 다 보내면 합쳐집니다. - 응답:
200,data는 갱신된 채널 객체
아래 요청은 채널에 환불요청 태그를 붙입니다.
curl -X POST "https://api.talk.zeroworks.ai/api/public/v1/channels/c1d2.../tags" \
-H "Authorization: Bearer ztpat_..." \
-H "Content-Type: application/json" \
-d '{ "tags": ["환불요청"] }'
tags (이름) | 사전에 없는 이름이면 사전에 추가됩니다. 임의 이름을 계속 만들면 태그 목록이 늘어나고 정리는 대시보드에서만 됩니다 |
tag_ids (id) | 현재 이름으로 해석되므로 그 사이 태그 이름이 바뀌어도 의도한 태그가 붙습니다. 삭제된 태그 id는 오류가 아니라 조용히 무시됩니다. 지운 태그가 되살아나지 않게 하기 위함입니다 |
부여는 멱등합니다. 이미 붙어 있는 태그를 다시 보내도 중복되지 않고 아무것도 바뀌지 않습니다.
대화 한 건이 가질 수 있는 태그 수에는 플랜별 상한이 있습니다. 넘으면 422입니다.
해제
DELETE /channels/{id}/tags?name=<태그> — 행위자를 지정하려면 &actor_member_id=<id>를 더합니다.
- Scope:
channels:write(권한 요구사항은 부여와 같습니다) - 태그명은 쿼리 파라미터로 전달하며, 여러 개는
name을 반복합니다(?name=a&name=b). 쉼표로 이어붙이면 그 전체가 태그 이름 하나로 해석됩니다. 하나도 없으면400 INVALID_QUERY입니다. - 해제는 이름만 받습니다.
tag_ids로 붙였다면 상담 태그에서 현재 이름을 조회해 그 이름으로 떼세요. - 응답:
200,data는 갱신된 채널 객체
아래 요청은 채널에서 환불요청과 VIP 태그를 뗍니다.
curl -X DELETE "https://api.talk.zeroworks.ai/api/public/v1/channels/c1d2.../tags?name=환불요청&name=VIP" \
-H "Authorization: Bearer ztpat_..."
해제도 멱등합니다. 붙어 있지 않은 태그를 떼도 오류가 아닙니다. 해제는 그 상담에서만 떼는 것이고 태그 사전에서 지우지 않습니다.
태그 변경은 outbound 웹훅을 발생시키지 않습니다. 이 페이지의 다른 조작(종료·재개·배정)과 달리 대응하는 웹훅 이벤트가 없으므로, 변경 결과는 응답의 data.tags 또는 채널 조회로 확인하세요.
상담 태그 부여 (API) · 상담 태그 해제 (API) 항목으로, 대상 상담·사용한 토큰(상담원을 지정했다면 그 상담원도)·요청한 태그와 그 결과 상담에 남은 태그가 기록됩니다. 태그가 아주 많으면 앞부분만 표본으로 남고 생략된 개수가 함께 적힙니다. 대시보드에서 직접 붙인 태그에는 이 기록이 남지 않습니다.
상담 메모 쓰기
상담 건마다 붙는 상담 메모(상담원 화면 정보 패널의 「상담 메모」 칸, 최대 500자)를 씁니다. 고객 단위로 남는 고객 메모와는 다른 칸입니다.
PUT /channels/{id}/consultation-memo
| 필드 | 타입 | 설명 |
|---|---|---|
mode | replace | append | 필수. replace는 메모 전체를 바꾸고, append는 기존 메모 뒤에 줄을 바꿔 붙입니다 |
memo | string | 필수. replace에서 빈 문자열("")을 보내면 메모를 지웁니다. append에서는 비울 수 없습니다 |
actor_member_id | string · 선택 | 행위자. 생략하면 토큰 자신입니다 |
아래 요청은 상담원이 써 둔 메모 뒤에 요약 한 줄을 붙입니다.
curl -X PUT "https://api.talk.zeroworks.ai/api/public/v1/channels/c1d2.../consultation-memo" \
-H "Authorization: Bearer ztpat_..." \
-H "Content-Type: application/json" \
-d '{ "mode": "append", "memo": "요약: 교환 접수, 회수 기사 배정 요청" }'
응답: 200. consultation_memo는 저장된 메모 전체이고, 지웠으면 null입니다.
{
"success": true,
"data": {
"channel_id": "c1d2e3f4-...",
"consultation_memo": "배송 지연 문의, 고객 통화 완료\n요약: 교환 접수, 회수 기사 배정 요청"
}
}
상담 메모는 이 응답에서만 돌려줍니다. 채널 조회 응답에는 들어가지 않습니다.
상담 메모 쓰기의 동작
| 이어붙이기 | 기존 메모가 없으면 memo만 저장하고, 있으면 기존 메모 + 줄바꿈 + memo를 저장합니다 |
| 500자 제한 | 저장될 결과 전체를 기준으로 셉니다. 넘으면 422 VALIDATION_FAILED(error.details.consultation_memo)를 반환하고 아무것도 저장하지 않습니다. 줄바꿈·탭 외의 제어 문자도 같은 오류입니다 |
| 종료된 상담 | 쓸 수 있습니다. 상담 상태는 바뀌지 않습니다 |
| 같은 값 | 결과가 지금 메모와 같으면 저장하지 않고 200을 반환합니다 |
| 동시 저장 | 이 API의 저장이 상담원 저장과 겹쳐 버전 충돌이 나면 메모를 다시 읽고, append는 그 최신 메모 뒤에 붙이며 replace는 보낸 값으로 바꿉니다. 반대로 상담원이 API 저장 전에 열어 둔 메모 칸을 API 저장 뒤에 저장하면, 대시보드는 칸 전체를 보내므로 API가 쓴 내용이 지워질 수 있습니다 |
append는 멱등하지 않습니다같은 요청을 두 번 보내면 두 번 붙습니다. 웹훅은 같은 이벤트를 두 번 이상 보낼 수 있으므로, 웹훅을 받아 메모를 쓰는 연동은 event_id로 이미 처리한 이벤트를 걸러 낸 뒤 호출하세요. 응답을 받지 못해 성공했는지 모르면, 다시 보내기 전에 대시보드에서 메모를 확인하세요.
상담 메모 쓰기의 권한
행위자를 생략하면 channels:write scope만으로 모든 상담의 메모를 쓸 수 있습니다. 행위자를 지정하면 그 상담원에게 channel:operate 권한이 있어야 하고(없으면 403 FORBIDDEN), 그 상담원이 관리자 이상이거나 그 상담의 담당자 또는 참여 멤버여야 합니다(아니면 403 INSUFFICIENT_CHANNEL_PERMISSION). 대시보드에서 그 상담원이 상담 메모를 저장할 수 있는 조건과 같습니다. 상담이 없거나 상담원의 접근 범위 밖이면 404입니다.
상담 메모 쓰기는 outbound 웹훅을 발생시키지 않고, 상담 화면에 시스템 메시지도 남기지 않습니다. actor_member_id는 권한 판정에만 쓰입니다. 감사 로그에는 행위자 지정 여부와 관계없이 토큰의 API 호출로만 남고(상담원 이름 없음), 메모 내용은 기록되지 않습니다.
상담 메모 쓰기가 안 될 때
| 코드 | 발생 조건 | 대응 |
|---|---|---|
400 INVALID_BODY | JSON 형식 오류, 본문에 JSON 객체가 하나가 아님, mode나 memo가 없음, mode가 replace·append가 아님, append인데 memo가 빈 문자열 | 본문을 고쳐 다시 보냅니다 |
403 FORBIDDEN | 지정한 상담원에게 channel:operate 권한이 없음 | 권한이 있는 상담원을 지정하거나 actor_member_id를 생략합니다 |
403 INSUFFICIENT_CHANNEL_PERMISSION | 지정한 상담원이 관리자 이상도, 그 상담의 담당자·참여 멤버도 아님 | 위와 같습니다 |
404 CHANNEL_NOT_FOUND | 상담이 없거나 삭제됐거나, 지정한 상담원의 접근 범위 밖 | 채널 ID와 행위자를 확인합니다 |
422 VALIDATION_FAILED | 저장될 메모가 500자를 넘거나 제어 문자를 포함함. 아무것도 저장하지 않습니다 | replace로 전체를 다시 쓰거나 대시보드에서 메모를 정리합니다 |
422 INVALID_ACTOR_MEMBER | actor_member_id가 UUID가 아니거나 이 워크스페이스의 멤버가 아님 | 멤버 조회로 ID를 확인합니다 |
503 PERMISSION_RESOLVE_FAILED | 지정한 상담원의 권한 정책을 읽지 못함 | 잠시 뒤 같은 요청을 다시 보냅니다 |
참고
- 답장과 채널 조작의 결과는 모두 Outbound 웹훅으로 반영됩니다. 단, 태그 변경과 상담 메모 쓰기는 예외입니다. 콘솔 전체 흐름은 상담 콘솔 임베드 가이드를 참고하세요.
- 현재 첨부 전송, 내부 메모 전송, 리치 블록·버튼은 지원하지 않습니다 (
text·markdown본문만). 알려진 제약을 참고하세요.