연락처
고객(연락처)을 읽고 쓰는 API입니다. 목록·단건·태그 조회는 contacts:read, 생성·수정·태그 부여·해제·SMS 수신 동의는 contacts:write scope가 필요합니다. 외부 시스템의 고객 ID를 external_id로 넘기면 제로톡 연락처와 1:1로 동기화할 수 있습니다.
목록 조회
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가 아니라 조회에 사용한 값으로 유지하세요. 응답값으로 덮어쓰면 다음 동기화 때 그 사람을 찾지 못합니다.
아래 명령은 이름·이메일·전화에 hong이 들어 있는 연락처를 50건까지 조회합니다.
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..." }
}
응답 필드
| 필드 | 타입 | 설명 |
|---|---|---|
id | string | 연락처 UUID |
external_id | string | 외부 시스템의 고객 ID. 값이 없으면 키가 생략됩니다. |
email | string | 이메일. 값이 없으면 키가 생략됩니다. |
phone | string | 전화번호. 값이 없으면 키가 생략됩니다. |
display_name | string | 표시용 이름. 이름 → 이메일 → 전화 순으로 값이 있는 첫 항목이고, 모두 없으면 "Anonymous"입니다. |
avatar_url | string | 프로필 이미지 URL. 값이 없으면 키가 생략됩니다. 읽기 전용입니다. |
is_member | boolean | 로그인한 멤버(인증된 사용자)인지, 익명·식별된 방문자인지 나타냅니다. |
tags | string[] | 연락처에 부여된 태그 이름. 없으면 빈 배열입니다. |
last_seen_at | string | 마지막으로 접속한 시각(RFC3339). 값이 없으면 키가 생략됩니다. |
created_at | string | 생성 시각(RFC3339) |
updated_at | string | 마지막 수정 시각(RFC3339) |
값이 없는 선택 필드는 null이 아니라 키 자체가 생략됩니다. "key" in obj로 존재 여부를 판별하세요. 위 예시는 이메일·전화·last_seen_at만 있는 연락처라 나머지 선택 필드의 키가 빠져 있습니다.
단건 조회
GET /contacts/{id} — 연락처 하나를 조회합니다. {id}는 연락처 UUID입니다.
- Scope:
contacts:read
curl "https://api.talk.zeroworks.ai/api/public/v1/contacts/a1b2c3d4-..." \
-H "Authorization: Bearer ztpat_..."
응답 — data에 목록 조회와 같은 필드의 연락처 객체 하나가 들어 있습니다.
{
"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는 태그 이름의 문자열 배열이며, 태그가 없으면 빈 배열([])입니다. 페이지네이션이 없으므로 meta는 포함하지 않습니다.
curl "https://api.talk.zeroworks.ai/api/public/v1/contacts/a1b2c3d4-.../tags" \
-H "Authorization: Bearer ztpat_..."
응답
{
"success": true,
"data": ["vip", "신규문의"]
}
태그의 정의(색상·사용 연락처 수 등)가 필요하면 GET /contact-tags를 사용하세요. 이 엔드포인트는 이름만 반환합니다.
생성·수정 (contacts:write)
연락처를 생성·수정하고, 태그와 마케팅 SMS 수신 동의를 관리하는 쓰기 엔드포인트입니다. 모두 contacts:write scope가 필요하며, 이 scope는 contacts:read를 포함하지 않습니다.
PAT는 워크스페이스 전체를 읽고 쓸 수 있는 비밀 값이므로, 쓰기 엔드포인트는 BFF(서버 사이드)에서만 호출하세요. Public API는 CORS를 허용하지 않습니다.
연락처 생성
POST /contacts — 연락처를 생성합니다.
- Scope:
contacts:write - Body: 아래 필드만 받습니다. 그 외 필드를 보내면
400 INVALID_JSON입니다.
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
external_id | string | 선택 | 외부 시스템 ID(최대 255자). 워크스페이스 안에서 유일해야 하며, 이미 쓰이는 값이면 409입니다. 병합으로 사라진 연락처가 쓰던 값도 계속 점유하므로, 어느 연락처의 응답에도 그 값이 보이지 않아도 409가 날 수 있습니다. |
email | string | 선택 | 이메일(최대 255자). 이메일 형식을 검증합니다. |
phone | string | 선택 | 전화번호(최대 50자) |
name | string | 선택 | 이름(최대 100자) |
아래 명령은 external_id가 user-42인 연락처를 만듭니다.
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, data는 목록 조회와 같은 연락처 객체입니다.
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, 갱신된 연락처 객체입니다.
- 필드를 생략하거나
null로 보내면 변경하지 않습니다. - 빈 문자열
""로 보내면 해당 필드를 삭제합니다.email·phone은 비우고,name은 자동 닉네임으로 되돌립니다. 일반적인 REST 관례(null로 비움)와 반대이니 주의하세요. external_id는 생성할 때만 지정할 수 있고 이후에는 바꿀 수 없습니다. 요청 본문에서 받지 않습니다.avatar_url은 보안상 허용된 CDN 도메인만 받으므로 쓰기 대상에서 제외되고, 읽기로만 노출됩니다.
태그 부여
POST /contacts/{id}/tags — 연락처에 태그를 부여합니다. 이미 붙어 있는 태그를 다시 보내도 결과가 같습니다(멱등).
- Scope:
contacts:write - Body:
{ "tags": ["vip", "newsletter"] }.tags는 1개 이상이어야 하고, 태그 하나는 최대 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
아래 명령은 vip와 newsletter 태그를 한 번에 해제합니다.
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 INVALID_QUERY입니다.
마케팅 SMS 수신 동의
PUT /contacts/{id}/sms-consent — 연락처의 마케팅 SMS 수신 동의를 설정합니다.
- Scope:
contacts:write - Body:
{ "opted_in": false, "reason": "고객 요청" }.opted_in(boolean)은 필수입니다.reason(최대 500자)은 선택이며, 수신 거부(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 응답에 포함되지 않으므로(쓰기 전용), 대시보드에서 확인하세요.
오류
| HTTP | code | 발생 조건 |
|---|---|---|
| 400 | INVALID_QUERY | limit이 1~100 범위를 벗어남(값을 보정하지 않습니다), cursor가 만료되었거나 형식이 틀림, q와 external_id를 함께 보냄, 태그 해제에서 name이 없음 |
| 400 | INVALID_INPUT | 조회 엔드포인트의 {id}가 UUID 형식이 아님 |
| 400 | INVALID_JSON | 본문이 JSON이 아니거나, 쓰기 엔드포인트가 받지 않는 필드가 들어 있음 |
| 404 | NOT_FOUND | 조회하려는 연락처가 없거나 이 워크스페이스에 속하지 않음 |
| 404 | CONTACT_NOT_FOUND | 수정·태그·SMS 동의의 {id} 연락처가 없음 |
| 409 | EXTERNAL_ID_ALREADY_EXISTS | 생성할 때 external_id가 이미 쓰이고 있음 |
| 422 | VALIDATION_FAILED | 이메일 형식 오류, 필드 길이 초과, 태그 형식·개수 제한 초과, opted_in 누락. 어느 필드인지는 error.details에 있습니다. |
공통 오류는 에러 문서를 참고하세요.