본문으로 건너뛰기

연락처

목록 조회

GET /contacts — 연락처 목록을 cursor pagination으로 조회합니다.

  • Scope: contacts:read
  • 쿼리: limit(기본 50, 최대 100), cursor, q(이름·이메일·전화 부분 검색), external_id(정확 일치 조회 — q와 동시 사용 불가, 0 또는 1건 반환)

정렬은 created_at 내림차순입니다(최신 연락처가 먼저).

q는 대소문자를 구분하지 않는 부분 일치(substring)이며, q에 포함된 % 또는 _는 와일드카드가 아니라 리터럴 문자로 검색됩니다.

external_id는 외부 시스템의 자체 ID로 연락처를 되찾을 때 씁니다. 생성(POST /contacts)이 409(external_id 중복)를 반환하면, GET /contacts?external_id=...로 기존 연락처의 UUID를 찾아 PATCH로 전환하세요.

이 조회는 생성이 중복을 판정하는 것과 같은 기준을 씁니다. 두 연락처가 하나로 병합되면 사라진 쪽이 쓰던 external_id도 살아남은 연락처로 계속 조회됩니다. 즉 409가 나는 값은 언제나 이 조회로 해당 연락처를 찾을 수 있습니다.

응답의 external_id는 조회에 쓴 값이 아닐 수 있습니다

응답에 실리는 external_id그 연락처 자신의 대표값입니다. 병합된 연락처는 여러 개의 external_id로 조회되지만 응답에는 대표값 하나만 실리고, 대표값이 없으면 아래 설명대로 키 자체가 생략됩니다.

로컬 매핑(자체 ID → 연락처 UUID)의 키는 응답의 external_id가 아니라 조회에 사용한 값으로 유지하세요. 응답값으로 덮어쓰면 다음 동기화 때 그 사람을 찾지 못합니다.

curl "https://api.talk.zeroworks.ai/api/public/v1/contacts?limit=50&q=hong" \
-H "Authorization: Bearer ztpat_..."

응답

{
"success": true,
"data": [
{
"id": "a1b2c3d4-...",
"email": "hong@example.com",
"phone": "+821012345678",
"display_name": "홍길동",
"is_member": false,
"tags": ["vip"],
"last_seen_at": "2026-05-01T10:00:00Z",
"created_at": "2026-01-01T00:00:00Z",
"updated_at": "2026-05-01T10:00:00Z"
}
],
"meta": { "has_more": true, "next_cursor": "eyJ..." }
}

display_name은 이름 → 이메일 → 전화 → "Anonymous" 순으로 결정되는 표시용 이름입니다.

is_member는 해당 연락처가 로그인한 멤버(인증된 사용자)인지, 익명·식별된 방문자인지를 나타냅니다.

external_id·email·phone·avatar_url·last_seen_at은 값이 없으면 null이 아니라 키가 생략됩니다 ("key" in obj로 판별). 위 예시는 이메일·전화·last_seen_at만 있는 연락처라 나머지 선택 필드 키가 빠져 있습니다.

단건 조회

GET /contacts/{id} — 단일 연락처를 조회합니다. {id}는 연락처 UUID입니다.

  • Scope: contacts:read
{
"success": true,
"data": {
"id": "a1b2c3d4-...",
"display_name": "홍길동",
"email": "hong@example.com",
"is_member": false,
"tags": ["vip"],
"created_at": "2026-01-01T00:00:00Z",
"updated_at": "2026-05-01T10:00:00Z"
}
}

태그 목록

GET /contacts/{id}/tags — 연락처에 부여된 태그 이름 목록을 조회합니다.

  • Scope: contacts:read

data는 연락처에 붙은 태그 이름의 문자열 배열입니다(연락처의 tags 컬럼을 그대로 반환). 페이지네이션이 없어 meta 필드는 포함되지 않으며, 태그가 없으면 빈 배열([])입니다.

{
"success": true,
"data": ["vip", "신규문의"]
}

태그의 정의(색상·사용 횟수 등 객체 형태)가 필요하면 GET /contact-tags를 사용하세요. 이 엔드포인트는 이름만 반환합니다.

생성·수정 (contacts:write)

연락처를 생성·수정하고 태그와 마케팅 SMS 수신동의를 관리하는 쓰기 엔드포인트입니다. 모두 contacts:write scope가 필요합니다 (read와 별개 — write가 read를 포함하지 않습니다).

PAT는 워크스페이스 전체를 읽고 쓸 수 있는 비밀 값이므로, 쓰기 엔드포인트는 BFF(서버 사이드)에서만 호출하세요. Public API는 CORS를 허용하지 않습니다.

연락처 생성

POST /contacts — 연락처를 생성합니다.

  • Scope: contacts:write
  • Body (아래 필드만 허용 — 그 외 필드를 보내면 400)
필드타입설명
external_idstring · 선택외부 시스템 ID. 워크스페이스 내 유일 — 이미 쓰이는 값이면 409. 병합으로 사라진 연락처가 쓰던 값도 계속 점유하므로, 어느 연락처의 응답에도 그 값이 보이지 않아도 409가 날 수 있습니다
emailstring · 선택이메일 (형식 검증)
phonestring · 선택전화번호
namestring · 선택이름 (최대 20자)
curl -X POST "https://api.talk.zeroworks.ai/api/public/v1/contacts" \
-H "Authorization: Bearer ztpat_..." \
-H "Content-Type: application/json" \
-d '{ "external_id": "user-42", "name": "홍길동", "email": "hong@example.com" }'

응답201, body는 목록 조회와 같은 연락처 객체.

멱등성

external_id를 지정하면 중복 생성이 409로 막혀 멱등 동기화에 쓸 수 있습니다. 409를 받으면 GET /contacts?external_id=...로 기존 연락처를 찾아 이어가세요 — 병합으로 사라진 연락처가 쓰던 값이어도 찾을 수 있습니다.

external_id 없이 생성하면 멱등하지 않아 재시도 시 중복 연락처가 생깁니다 — 원본 201 응답의 id를 보관하세요.

연락처 수정

PATCH /contacts/{id} — 연락처를 부분 수정합니다.

  • Scope: contacts:write
  • Body: email, phone, name (생성과 동일 검증)
curl -X PATCH "https://api.talk.zeroworks.ai/api/public/v1/contacts/a1b2c3d4-..." \
-H "Authorization: Bearer ztpat_..." \
-H "Content-Type: application/json" \
-d '{ "name": "새 이름", "email": "" }'

응답200, 갱신된 연락처 객체. 없는 ID는 404.

빈 문자열은 필드를 삭제합니다
  • 필드를 생략하거나 null로 보내면 → 변경 없음.
  • 빈 문자열 "" 로 보내면 → 해당 필드를 삭제 (email/phone은 비우고, name은 자동 닉네임으로 되돌립니다). 일반 REST 관례(null로 비움)와 반대이니 주의하세요.
  • external_id는 생성 시에만 지정하며 이후 변경 불가(요청 본문에서 받지 않습니다).
  • avatar_url은 보안상 허용된 CDN 도메인만 받으므로 쓰기 대상에서 제외(읽기로만 노출)됩니다.

태그 부여

POST /contacts/{id}/tags — 연락처에 태그를 부여합니다 (멱등).

  • Scope: contacts:write
  • Body: { "tags": ["vip", "newsletter"] } (태그당 최대 50자, 연락처당 최대 100개)
curl -X POST "https://api.talk.zeroworks.ai/api/public/v1/contacts/a1b2c3d4-.../tags" \
-H "Authorization: Bearer ztpat_..." \
-H "Content-Type: application/json" \
-d '{ "tags": ["vip"] }'

응답200, 갱신된 연락처 객체.

태그 해제

DELETE /contacts/{id}/tags?name=<태그> — 태그를 해제합니다 (멱등). 태그명은 쿼리 파라미터로 전달하며, 여러 개는 name을 반복합니다.

  • Scope: contacts:write
curl -X DELETE "https://api.talk.zeroworks.ai/api/public/v1/contacts/a1b2c3d4-.../tags?name=vip&name=newsletter" \
-H "Authorization: Bearer ztpat_..."

응답200, 갱신된 연락처 객체. name이 하나도 없으면 400. (태그명은 슬래시·공백·유니코드를 포함할 수 있어 경로가 아닌 쿼리로 받습니다.)

마케팅 SMS 수신동의

PUT /contacts/{id}/sms-consent — 연락처의 마케팅 SMS 수신동의를 설정합니다.

  • Scope: contacts:write
  • Body: { "opted_in": false, "reason": "고객 요청" } (opted_in 필수, reason 선택 — opt-out 사유로 기록)
curl -X PUT "https://api.talk.zeroworks.ai/api/public/v1/contacts/a1b2c3d4-.../sms-consent" \
-H "Authorization: Bearer ztpat_..." \
-H "Content-Type: application/json" \
-d '{ "opted_in": false, "reason": "고객 요청" }'

응답204 No Content. 현재 동의 상태는 공개 API 응답에 포함되지 않으므로(쓰기 전용) 대시보드에서 확인하세요.