본문으로 건너뛰기

회원 인증

memberIdmemberHash 를 함께 전달하면 로그인된 방문자를 식별해 이전 상담 내역을 이어서 보여주고, 상담원에게도 어떤 회원인지 표시할 수 있습니다. 성함·연락처는 물론 주문번호 같은 자사 업무 값도 함께 넘겨 상담원 화면에 띄울 수 있습니다.

memberHash 는 위변조 방지를 위해 자사 백엔드에서 HMAC-SHA256 으로 계산해야 합니다.

Secret Key 는 절대 클라이언트에 노출하지 마세요

Secret Key 가 노출되면 누구든 임의의 사용자로 위장해 채팅을 시작할 수 있습니다. 환경 변수나 시크릿 매니저에만 보관하고, 계산된 memberHash 결과값만 클라이언트로 전달하세요.

인증 흐름

1. 백엔드에서 HMAC 계산

memberHash = HMAC-SHA256(SECRET_KEY, memberId) — Secret Key 를 키로, memberId 를 데이터로 사용해 계산합니다. 결과는 hex (lowercase) 문자열입니다.

zerotalk.ts
import crypto from "node:crypto";

const SECRET_KEY = process.env.ZEROTALK_SECRET_KEY!;

export function getMemberHash(memberId: string): string {
return crypto
.createHmac("sha256", SECRET_KEY)
.update(memberId)
.digest("hex");
}

계산 결과가 서버와 어긋나지 않으려면 세 가지를 지켜주세요.

  • Secret Key 문자열을 그대로 키로 사용합니다. 64자 hex 형태이지만 hex 디코딩하지 마세요.
  • 결과는 소문자 hex 여야 합니다. 대문자로 만들면 검증에 실패합니다.
  • memberId 는 위젯으로 전달하는 문자열과 바이트 단위로 동일해야 합니다. 앞뒤 공백이나 대소문자가 다르면 다른 해시가 나옵니다.

2. 클라이언트에 주입

서버 템플릿에서 계산된 값을 init 옵션으로 전달합니다. 위젯 설치는 설치 가이드의 CDN 스크립트 방식을 그대로 사용합니다.

<script src="https://cdn.talk.zeroworks.ai/latest/chat-widget.iife.js"></script>
<script>
ZeroTalk.init({
pluginKey: "YOUR_PLUGIN_KEY",
apiBaseUrl: "https://api.talk.zeroworks.ai/api/v1",
wsUrl: "wss://api.talk.zeroworks.ai/ws/sdk",
memberId: "{{ user.id }}",
memberHash: "{{ member_hash }}",
profile: {
name: "{{ user.name }}",
phone: "{{ user.phone }}",
email: "{{ user.email }}",
},
});
</script>
memberId 가 있으면 identify() 를 따로 부르지 않아도 됩니다

페이지를 열 때 이미 로그인 상태라면 init()memberId 를 넘기는 것만으로 충분합니다. 위젯이 접속 직후 회원 식별을 자동으로 수행해 profilecustomFields 를 서버로 보냅니다. identify()화면을 떠나지 않고 로그인하는 경우(익명 → 회원 전환)에만 필요합니다.

3. 회원 정보 전달

회원의 정보는 성격이 다른 두 개의 자리로 나뉘어 들어가고, 검증 규칙이 서로 정반대입니다.

자리규칙상담원 화면 노출
profile자유형. 아무 키나 넣어도 거부되지 않음표준 키만 노출
customFields대시보드에 미리 정의한 키만 허용. 모르는 키를 넣으면 실패정의한 필드가 모두 노출

profile — 성함·연락처

profile 의 아래 네 개는 표준 키로, 고객 정보의 정규 항목에 채워집니다. 철자를 정확히 지켜주세요 (avatar_url 은 snake_case 입니다).

상담원 화면
name성함
phone휴대폰
email이메일
avatar_url프로필 이미지
ZeroTalk.init({
// ...
memberId: "user_12345",
memberHash: "3f2a...", // 소문자 hex 64자
profile: {
name: "홍길동",
phone: "010-1234-5678",
email: "hong@example.com",
},
});
표준 키가 아닌 profile 값은 상담원에게 보이지 않습니다

profileorderNumber 같은 임의의 키를 넣으면 저장은 되지만 상담원 화면에는 표시되지 않습니다. 주문번호처럼 상담원이 봐야 하는 값은 반드시 다음 절의 customFields 를 사용하세요.

profile 값의 한도는 키 100자, 문자열 값 10,000자, 전체 50KB이며, 표준 키 외 속성 개수는 요금제에 따라 제한됩니다.

4. 주문번호 같은 업무 값 넘기기 (customFields)

주문번호·등급·가입경로처럼 자사에만 있는 값은 customFields 로 보냅니다. 대시보드에 필드를 먼저 만들어야 합니다.

4-1. 대시보드에서 필드 정의 (선행 필수)

관리자 권한으로 대시보드 고객 페이지 → 필드 관리 에서 커스텀 필드를 만듭니다.

  • 필드 키 — 영소문자로 시작하고 영소문자·숫자·밑줄만 사용합니다 (^[a-z][a-z0-9_]{0,99}$). 예: order_number. 만든 뒤에는 바꿀 수 없습니다.
  • 표시 이름 — 상담원 화면에 보이는 이름입니다. 한글을 써도 됩니다. 예: 주문번호
  • 필드 타입 — 텍스트 / 숫자 / 날짜 / 참거짓 / 선택 / 다중선택

4-2. 위젯에서 값 전달

customFields 의 키는 위에서 정한 필드 키와 글자 그대로 같아야 합니다. init 옵션 이름(memberId, customFields 등)은 camelCase 이지만, profilecustomFields 안쪽의 키는 변환되지 않고 그대로 전달됩니다.

ZeroTalk.init({
// ...
memberId: "user_12345",
memberHash: "3f2a...",
profile: { name: "홍길동", phone: "010-1234-5678" },
customFields: {
order_number: "ORD-2026-0001", // ✅ 정의한 필드 키와 동일
// orderNumber: "..." // ❌ 정의되지 않은 키 → 아래 경고 참조
},
});
정의하지 않은 키를 보내면 아무 오류 없이 조용히 실패합니다

customFields 에 대시보드에 없는 키가 하나라도 있으면 회원 식별 요청 전체가 거부됩니다. 그런데 화면에는 아무 오류도 나타나지 않고 채팅은 정상으로 동작합니다. 값이 보이지 않는다면 브라우저 개발자 도구 콘솔에서 다음 경고를 확인하세요.

[ZeroTalk] identify failed: ...

무엇이 사라지는지는 호출 방식에 따라 다릅니다.

호출성함·연락처커스텀 필드
init({ memberId, profile, customFields })저장됨저장 안 됨
identify({ memberId, profile, customFields })저장 안 됨저장 안 됨

즉 세션 도중 로그인하는 경우(identify())에는 키 하나만 틀려도 성함과 연락처까지 함께 유실됩니다.

필드는 나중에 추가해도 되지만, 정의가 없는 상태에서 보낸 커스텀 필드 값은 저장되지 않았으므로 되살아나지 않습니다. 필드를 만든 뒤 값을 다시 보내야 상담원 화면에 나타납니다.

4-3. 상담원이 보는 것

상담 화면 오른쪽 고객 정보 패널에 아래와 같이 표시됩니다.

어디에사전 설정
성함고객 정보 상단불필요
휴대폰·이메일고객 정보불필요
회원 아이디 (memberId)고객 정보불필요
주문번호 등커스텀 필드 섹션대시보드 정의 필요

5. 로그인·로그아웃 전환

상황별로 호출할 것은 아래 하나뿐입니다.

상황호출
페이지를 열 때 이미 로그인 상태init({ memberId, memberHash, ... })
화면 이동 없이 로그인 (SPA 등)identify({ memberId, memberHash, ... })
로그아웃logout()

화면 이동 없이 로그인·로그아웃이 일어나는 경우에는 위젯을 다시 만들지 말고 아래 두 함수를 호출하세요.

// 로그인 — 익명 세션이 회원 세션으로 승계되며 이전 상담 내역이 이어집니다
ZeroTalk.identify({
memberId: "user_12345",
memberHash: "3f2a...",
profile: { name: "홍길동", phone: "010-1234-5678" },
customFields: { order_number: "ORD-2026-0001" },
});

// 로그아웃 — 세션을 비우고 익명 상태로 되돌립니다
ZeroTalk.logout();
destroy()init() 으로 회원을 바꾸지 마세요

destroy() 는 저장된 세션을 지우지 않기 때문에 다시 init() 해도 이전 방문자의 상담 내역이 그대로 이어집니다. 회원을 전환하려면 identify(), 로그아웃하려면 logout() 을 사용하세요.

6. 서명된 속성 (권장) — identityToken 콜백

memberHashmemberId 하나만 서명합니다. profilecustomFields 는 서명되지 않으므로 서버가 그대로 신뢰하지 않을 수 있습니다. 속성까지 서명하려면 자사 백엔드가 Sealed Identity Token(SIT) 을 발급하고, 위젯이 이를 identityToken 콜백으로 전달합니다.

6-1. 토큰 계약

항목
서명 알고리즘HS256 — 그 외 alg(none, RS256 등)는 거부됩니다
서명 키Plugin Key 의 Secret Key(memberHash 계산에 쓰는 키와 동일). 64자 hex 문자열을 그대로 키로 사용하고, hex 디코딩하지 마세요
audzerotalk:widget-identity — 이 문자열과 정확히 일치해야 합니다
sub회원 ID. 위젯에 전달하는 memberId 와 같아야 합니다
jti토큰마다 새로 생성하는 난수(UUID) — 1회용이며, 재사용 시 거부됩니다. 요청이 거부되어도 이미 소진되므로 재시도에는 새 토큰이 필요합니다 (아래 참고)
iat발급 시각(unix seconds), 필수. 현재보다 60초 넘게 미래면 거부됩니다
exp만료 시각(unix seconds), 필수. exp - iat 는 0보다 크고 600초(10분) 이하여야 합니다
profile위젯 profile 로 보내려던 값을 여기에 담습니다
custom_fields위젯 customFields 로 보내려던 값을 여기에 담습니다. 클레임 이름은 snake_case 입니다(customFields 가 아니라 custom_fields)
속성은 반드시 토큰 안으로 옮기세요 — 밖에 남기면 저장되지 않습니다

서명 모드에서는 서버가 서명된 클레임만 신뢰합니다. 토큰과 함께 서명되지 않은 최상위 profile/customFields 를 보내면 요청 전체가 UNSIGNED_FIELDS_PRESENT 로 실패하고, 위젯은 토큰을 첨부할 때 최상위 profile/custom_fields 를 자동으로 제거합니다 — 즉 토큰의 profile/custom_fields 클레임에 값을 넣지 않으면 어떤 속성도 저장되지 않습니다. (memberId 는 그대로 전송되며 sub 와 같기만 하면 됩니다.)

토큰 안의 커스텀 필드도 대시보드 정의가 먼저 필요합니다 — 실패한 요청은 토큰을 소진합니다

서명된 custom_fields 클레임에도 4-1 에서 만든 필드 키만 쓸 수 있습니다. 정의되지 않은 키가 하나라도 있으면 — 서명 여부와 무관하게 — identify 요청 전체가 거부됩니다.

이때 토큰의 jti거부된 요청에서도 소진됩니다. 같은 토큰으로 다시 시도하면 1회용 규칙에 걸려 거부되므로, 재시도할 때는 identityToken 콜백이 반드시 새 토큰을 발급해야 합니다.

6-2. 백엔드: 토큰 발급 엔드포인트

// 자사 백엔드 — GET /api/zerotalk/identity-token (로그인 세션 필요)
import jwt from "jsonwebtoken";
import { randomUUID } from "node:crypto";

app.get("/api/zerotalk/identity-token", (req, res) => {
const user = req.session.user; // 자사 서버가 인증한 회원
const now = Math.floor(Date.now() / 1000);
const token = jwt.sign(
{
sub: user.id, // 위젯의 memberId 와 동일해야 함
aud: "zerotalk:widget-identity",
jti: randomUUID(), // 발급마다 새로 생성 — 1회용
iat: now,
exp: now + 300, // exp - iat ≤ 600
profile: { name: user.name, phone: user.phone, email: user.email },
custom_fields: { order_number: user.lastOrderId },
},
process.env.ZEROTALK_SECRET_KEY, // Plugin Key 의 Secret Key (서버 전용)
{ algorithm: "HS256" },
);
res.json({ token });
});

6-3. 위젯: 콜백 배선

ZeroTalk.init({
pluginKey: "pk_live_...",
memberId: "member-123", // 토큰의 sub 와 같은 값
memberHash: "<HMAC-SHA256(memberId)>",
identityToken: async () => {
const r = await fetch("/api/zerotalk/identity-token");
return (await r.json()).token; // 호출마다 새 토큰
},
// profile / customFields 는 여기가 아니라 토큰 클레임에 넣습니다.
});

콜백은 identify 호출마다 실행됩니다. 토큰은 1회용(jti)이고 수명이 짧으므로(최대 10분) 캐싱하지 마세요. 서명 모드는 memberId 를 함께 설정했을 때만 켜지며, 익명 방문자의 입력은 이전과 동일하게 처리됩니다. 콜백이 예외를 던지면 위젯은 토큰 없이 진행하며, 강제 모드에서는 그 요청의 속성이 드롭됩니다.

7. 보안 — profilecustomFields 는 서명되지 않습니다

HMAC 서명이 보호하는 값은 memberId 하나뿐입니다. profilecustomFields 는 같은 요청에 실려 오지만 서명 대상이 아니며, 서버는 이를 그대로 저장합니다.

위조 가능 여부
memberId불가 — Secret Key 없이는 유효한 memberHash 를 만들 수 없습니다
profile.name · phone · email · avatar_url가능
customFields 의 모든 값가능

자기 계정으로 정상 로그인한 방문자가 브라우저에서 이 값들을 고쳐 보내도 서명 검증은 통과합니다.

위젯으로 전달된 값은 참고용 표시값으로만 취급하세요

customFields 로 받은 주문번호는 상담원이 눈으로 확인하는 용도로는 충분하지만, 그 값을 신뢰해 환불·배송지 변경 같은 처리를 해서는 안 됩니다. 권위가 필요한 값은 상담원이 memberId 로 자사 백엔드를 다시 조회해 확인하세요.

강제 모드: 워크스페이스가 이 Plugin Key 에 서명 강제(관리자 설정)를 켜면, 서명되지 않은 profile/customFields 는 서버에서 드롭됩니다 — 이후 회원 속성은 identityToken 콜백으로만 반영됩니다. identify/activate 시점에 드롭이 발생하면 위젯 콘솔에 경고가 남지만, 모든 드롭이 경고를 남기는 것은 아닙니다. 로그인 전에 채팅을 시작한 방문자처럼 익명 진입으로 들어온 요청은 경고 없이 드롭될 수 있습니다. 콘솔 경고가 없다고 속성이 저장됐다는 뜻은 아니므로, 대시보드에서 고객 프로필을 직접 확인하세요.

cafe24 연동 워크스페이스: cafe24 연동 워크스페이스는 자체 식별 흐름을 사용하며 identityToken지원하지 않습니다(토큰은 무시됩니다). 서명 강제 역시 cafe24 워크스페이스에는 적용되지 않습니다.

8. 오류 코드

회원 인증이 실패하면 위젯의 접속 요청이 아래와 같이 응답합니다.

상황상태코드
memberId 는 있는데 memberHash 가 없음401INVALID_HASH
memberHash 가 서버 계산값과 다름401INVALID_HASH
memberHash 는 있는데 memberId 가 없음400INVALID_INPUT
pluginKey 누락 등 형식 오류422VALIDATION_FAILED
pluginKey 형식이 잘못됨401INVALID_KEY_FORMAT

memberIdmemberHash 는 항상 함께 전달하세요. 익명 상태에서는 둘 다 생략합니다.

회원 식별 실패는 조용히 지나갑니다

접속 시점의 인증 실패는 위젯이 연결되지 않아 바로 드러나지만, identify() 실패와 customFields 검증 실패는 콘솔 경고만 남기고 채팅은 정상 동작합니다. 연동 검증 시 반드시 콘솔을 확인하세요.

네트워크 요청에서 보이는 필드 이름

개발자 도구로 요청을 들여다보거나 HTTP 를 직접 호출한다면, 실제 전송되는 필드 이름이 init 옵션 이름과 다르다는 점에 주의하세요.

요청전송되는 필드
POST /api/v1/sdk/bootplugin_key, member_id, hash, profile
POST /api/v1/sdk/identifymember_id, hash, profile, custom_fields (헤더에 X-Session-Token)
memberHashmember_hash 가 아니라 hash 로 전송됩니다

HTTP 를 직접 호출한다면 계산한 값을 hash 필드에 넣어야 합니다. 요청에 함께 보이는 member_hash카페24 연동 전용 필드로, 일반 워크스페이스에서는 검증되지 않습니다. member_hash 에만 넣으면 hash 누락으로 401 INVALID_HASH 가 납니다.

custom_fieldsidentify 요청에만 실립니다. boot 에는 없습니다.

자주 묻는 질문

익명으로 시작했다가 로그인 후 식별로 전환할 수 있나요?

네. 로그인 시점에 ZeroTalk.identify({ memberId, memberHash, ... }) 를 호출하면 익명 상태에서 나눈 대화가 회원 계정으로 이어집니다. 페이지를 새로 여는 경우라면 init()memberId 를 넘기는 것만으로 같은 결과가 됩니다.

Plugin Key 를 secret 으로 써도 되나요?

안 됩니다. Plugin Key(pk_live_…)는 공개 식별자이고, HMAC 계산에는 별도로 발급된 Secret Key 를 사용해야 합니다.

memberHash 가 매 요청마다 바뀌어야 하나요?

아니요. 같은 memberId 와 같은 Secret Key 면 항상 같은 해시가 나옵니다. 캐싱해도 무방합니다.

Secret Key 를 잃어버렸거나 유출됐다면?

Secret Key 는 Plugin Key 를 만들 때 한 번만 표시되고, 이후에는 다시 조회할 수 없습니다. 잃어버렸거나 유출됐다면 대시보드채팅 설정 → 연동 / 개발 → 연동 설정 → 웹사이트 카드 → Plugin Key 관리에서 새 Plugin Key 를 발급하세요.

주의

새 키를 발급하면 공개 Plugin Key 값도 함께 바뀝니다. 백엔드의 Secret Key 환경 변수뿐 아니라 클라이언트의 pluginKey 값도 교체해야 합니다. 또한 기존 키는 별도로 폐기(Revoke) 하기 전까지 계속 유효하므로, 유출 대응이라면 새 키로 교체한 뒤 반드시 이전 키를 폐기하세요.

주문번호가 상담원 화면에 안 보여요

세 가지를 확인하세요.

  1. 대시보드 고객 → 필드 관리에 해당 필드가 정의돼 있는지
  2. customFields 의 키가 정의한 필드 키와 글자 그대로 같은지 (orderNumberorder_number)
  3. profile 이 아니라 customFields 로 보내고 있는지

브라우저 콘솔에 [ZeroTalk] identify failed 가 있다면 2번일 가능성이 높습니다.