빠른 시작
Connectelly External API 는 2단계 인증을 사용합니다. API Key 로 단수명 access token 을 발급받고, 실제 호출은 그 토큰으로 합니다.
- 파트너 신청 후 API Key 받기 — 계약 완료 후 마스터가 키를 발급합니다.
- access token 발급 —
POST /api/external/v1/token - 토큰으로 호출 —
Authorization: Bearer <access_token>
# 1) 토큰 발급 (API Key 는 여기서만 사용)
curl -X POST https://connectelly.com/api/external/v1/token \
-H "X-Connectelly-Api-Key: ck_live_..."
# → {"access_token":"eyJ...","token_type":"Bearer","expires_in":1800,
# "event_id":"af2f95b4..."} ← eventId 는 여기서 받습니다
# 2) 발급받은 토큰으로 연락처 전송
curl -X POST https://connectelly.com/api/external/v1/events/<eventId>/contacts \
-H "Authorization: Bearer eyJ..." \
-H "Content-Type: application/json" \
-d '{"external_id":"REG-001","name":"John Smith","email":"john@example.com"}'
응답 200 이면 저장 완료. 앱 유저가 REG-001 QR 을 스캔하면 리드가 자동 캡처됩니다.
인증
2단계 인증
장수명 API Key 는 토큰 발급에만 쓰고, 실제 API 호출은 단수명 access token 으로 합니다. 평상시 트래픽에 키가 실리지 않아 파트너 서버 로그·프록시를 통한 노출이 사라집니다.
API Key
- 형식:
ck_live_<32 url-safe base64> POST /api/external/v1/token에서만X-Connectelly-Api-Key헤더로 사용- 행사 단위 발급. 다른 행사에는 사용 불가 (404 event_not_found)
- 서버 사이드 전용. 클라이언트 코드나 공개 저장소에 노출 금지
Access Token
- 수명 30분(1800초). 만료되면 다시 발급받으세요.
- 모든 API 호출에
Authorization: Bearer <access_token> - 키가 폐기되면 이미 발급된 토큰도 즉시 무효가 됩니다.
키 유출 시
관리자에게 즉시 알려 키를 폐기하세요. 폐기 즉시 해당 키와 그 키로 발급된 모든 토큰이 차단됩니다. 새 키를 재발급받으면 기존 키는 24시간 유예 후 만료되므로 무중단으로 교체할 수 있습니다.
IP 제한 (권장)
허용 IP/CIDR 를 등록하면 그 주소에서만 토큰을 발급받을 수 있습니다. 키가 유출돼도 다른 위치에서는 사용이 불가능합니다. 관리자에게 요청하세요.
토큰 API
API Key 를 단수명 access token 으로 교환합니다. API Key 를 헤더에 담는 엔드포인트는 이곳뿐이며, 나머지 모든 API 는 여기서 받은 토큰으로 호출합니다.
요청
POST /api/external/v1/token
| 헤더 | 필수 | 설명 |
|---|---|---|
X-Connectelly-Api-Key | 필수 | 발급받은 API Key (ck_live_...) |
요청 body 는 필요하지 않습니다.
curl -X POST https://connectelly.com/api/external/v1/token \
-H "X-Connectelly-Api-Key: ck_live_..."
응답 200
{
"access_token": "eyJhbGciOiJIUzM4NCJ9...",
"token_type": "Bearer",
"expires_in": 1800,
"expires_at": "2026-09-07T05:18:35Z",
"event_id": "af2f95b402794c8f9289360fc8765b8e"
}
| 필드 | 타입 | 설명 |
|---|---|---|
| access_token | string | 이후 모든 요청의 Authorization 헤더에 사용 |
| token_type | string | 항상 Bearer |
| expires_in | integer | 잔여 수명(초). 기본 1800 = 30분 |
| expires_at | string | 만료 시각 (ISO-8601 UTC) |
| event_id | string | 이 키에 연결된 행사 ID. 이후 모든 엔드포인트 경로의 {eventId} 에 그대로 사용 |
eventId 는 어디서 받나요?
API Key 는 행사 1개에 묶여 발급되므로 eventId 를 따로 관리할 필요가 없습니다. 두 가지 방법으로 확인할 수 있습니다.
- 토큰 응답의
event_id— 위 응답에서 바로 얻습니다. 하드코딩하지 말고 이 값을 쓰는 것을 권장합니다. - 키 발급 시 안내 — 담당자가 API Key 를 전달할 때 함께 알려드립니다.
다른 행사의 eventId 를 넣으면 404 event_not_found 가 반환됩니다.
오류
| HTTP | code | 원인 |
|---|---|---|
| 401 | auth_missing | 헤더 없음 |
| 401 | auth_invalid | 키가 인식되지 않음 |
| 401 | key_expired | 키 만료 (또는 재발급 유예 종료) |
| 401 | key_revoked | 키가 폐기됨 |
| 403 | ip_not_allowed | 허용되지 않은 IP |
운영 팁
- 매 요청마다 발급하지 말고 토큰을 캐시하세요. 만료 1~2분 전에 갱신하는 것을 권장합니다.
- 401
token_invalid를 받으면 토큰을 재발급받아 1회만 재시도하세요. - 토큰은 서버 메모리에만 두고 로그·URL 에 남기지 마세요.
연락처 등록·수정
연락처 1건을 등록하거나 수정합니다. external_id 가 이미 있으면 덮어씁니다(UPSERT). 등록 시점에 호출하면 앱 QR 스캔 시 리드가 자동 캡처됩니다.
POST /api/external/v1/events/{eventId}/contacts
요청 헤더
| 헤더 | 필수 | 설명 |
|---|---|---|
Authorization | 필수 | Bearer <access_token> |
Content-Type | 필수 | application/json |
Idempotency-Key | 선택 | 같은 키로 재시도하면 다시 처리하지 않고 첫 응답을 돌려줍니다. 멱등성 항목 참조 |
요청 body
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| external_id | string | 필수 | 등록 시스템의 고유 ID. 최대 100자. QR 값과 일치해야 합니다 |
| name | string | 선택 | 이름. 최대 200자 |
| string | 선택 | 이메일. 저장 시 AES-256-GCM 암호화 | |
| phone | string | 선택 | 전화번호. 저장 시 암호화 |
| company | string | 선택 | 회사명. 최대 200자 |
| position | string | 선택 | 직책. 최대 100자 |
| extra | object | 선택 | 자유 JSON. 파트너 확장 필드 |
curl -X POST https://connectelly.com/api/external/v1/events/<eventId>/contacts \
-H "Authorization: Bearer eyJ..." \
-H "Content-Type: application/json" \
-d '{
"external_id": "REG-001",
"name": "John Smith",
"email": "john@example.com",
"phone": "010-1234-5678",
"company": "Acme Inc.",
"position": "CEO",
"extra": { "booth": "A-12" }
}'
응답 200
방금 보낸 값을 그대로 돌려줍니다(전송 확인용). 필드 형식은 연락처 조회와 같습니다. 단, 이 응답의 qr_image_url 은 항상 null 입니다 — QR 이미지 URL 은 연락처 조회·목록으로 받으세요.
{
"id": "con_af2f95b4_REG-001",
"object": "external_contact",
"event_id": "af2f95b402794c8f9289360fc8765b8e",
"external_id": "REG-001",
"name": "John Smith",
"email": "john@example.com",
"phone": "010-1234-5678",
"company": "Acme Inc.",
"position": "CEO",
"extra": {"booth": "A-12"},
"created_at": "2026-09-07T04:50:38Z",
"updated_at": "2026-09-07T04:50:38Z",
"qr_image_url": null
}
오류
| HTTP | code | 원인 |
|---|---|---|
| 400 | validation_failed | external_id 누락 또는 필드 길이 초과 |
| 400 | invalid_json | 요청 본문이 올바른 JSON 이 아님 |
| 401 | token_missing / token_invalid | 토큰 없음 / 위조·만료 |
| 404 | event_not_found | 행사 없음 또는 키 범위 밖 |
| 409 | idempotency_conflict | 같은 Idempotency-Key 를 다른 요청에 사용 |
| 409 | idempotency_in_progress | 같은 Idempotency-Key 의 첫 요청이 아직 처리 중 |
| 429 | rate_limited | 속도 제한 초과 |
연락처 일괄 등록
최대 500건을 한 번에 UPSERT 합니다. 초기 데이터 적재나 야간 동기화에 사용하세요.
POST /api/external/v1/events/{eventId}/contacts/batch
요청 body
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| contacts | array | 필수 | 연락처 배열. 1~500건. 각 항목은 단건 등록과 동일한 형식 |
curl -X POST https://connectelly.com/api/external/v1/events/<eventId>/contacts/batch \
-H "Authorization: Bearer eyJ..." \
-H "Content-Type: application/json" \
-d '{
"contacts": [
{ "external_id": "REG-001", "name": "John Smith", "email": "john@example.com" },
{ "name": "Jane Doe", "email": "jane@example.com" }
]
}'
응답 200
예시의 두 번째 항목은 external_id 가 없어 그 건만 실패하고, 나머지는 저장됩니다.
{
"total": 2,
"created": 1,
"updated": 0,
"failed": 1,
"errors": [
{
"external_id": null,
"code": "validation_failed",
"message": "external_id is required.",
"param": "external_id"
}
]
}
| 필드 | 타입 | 설명 |
|---|---|---|
| total | integer | 요청에 담긴 총 건수 |
| created | integer | 신규 생성 건수 |
| updated | integer | 기존 덮어쓴 건수 |
| failed | integer | 저장되지 않은 건수 |
| errors | array | 실패한 항목만 (external_id, code, message, param). 모두 성공하면 빈 배열 |
주의
- 배열이 비어 있거나 500건을 넘으면 요청 전체가 400
validation_failed입니다. - 부분 성공을 허용합니다. 일부가 실패해도 올바른 항목은 저장됩니다.
errors의 항목만 고쳐 다시 보내세요. UPSERT 라 중복이 생기지 않습니다. - 대량 적재 시 429 를 피하려면 호출 제한 항목을 참고해 간격을 두세요.
Idempotency-Key헤더를 지원합니다. 타임아웃 시 같은 키로 재시도하면 한 번만 처리됩니다. 멱등성 항목 참조.
연락처 목록
이 행사에 저장된 연락처 전체를 한 번에 조회합니다. 보낸 데이터를 대사하거나, 명찰 인쇄용 QR 이미지를 일괄로 모을 때 씁니다.
GET /api/external/v1/events/{eventId}/contacts
curl https://connectelly.com/api/external/v1/events/<eventId>/contacts \
-H "Authorization: Bearer eyJ..."
응답 200
쿼리 파라미터와 페이지네이션은 없습니다. contacts 의 각 항목은 연락처 조회 응답과 필드가 같고, 이름·이메일·전화번호도 같은 방식으로 마스킹됩니다.
{
"total": 2,
"returned": 2,
"truncated": false,
"contacts": [
{
"id": "con_af2f95b4_REG-002",
"object": "external_contact",
"event_id": "af2f95b402794c8f9289360fc8765b8e",
"external_id": "REG-002",
"name": "J******e",
"email": "ja**@example.com",
"phone": null,
"company": null,
"position": null,
"extra": null,
"created_at": "2026-09-07T05:02:11Z",
"updated_at": "2026-09-07T05:02:11Z",
"qr_image_url": "https://cdn.connectelly.com/qr/af2f95b402794c8f9289360fc8765b8e/REG-002.png"
},
{
"id": "con_af2f95b4_REG-001",
"object": "external_contact",
"event_id": "af2f95b402794c8f9289360fc8765b8e",
"external_id": "REG-001",
"name": "J********h",
"email": "jo**@example.com",
"phone": "010-****-5678",
"company": "Acme Inc.",
"position": "CEO",
"extra": {"booth": "A-12"},
"created_at": "2026-09-07T04:50:38Z",
"updated_at": "2026-09-07T04:50:38Z",
"qr_image_url": "https://cdn.connectelly.com/qr/af2f95b402794c8f9289360fc8765b8e/REG-001.png"
}
]
}
| 필드 | 타입 | 설명 |
|---|---|---|
| total | integer | 이 행사에 저장된 전체 연락처 수 |
| returned | integer | 이번 응답에 담긴 연락처 수 (최대 10,000) |
| truncated | boolean | true 면 10,000건 상한에 걸려 최근 10,000건만 반환된 것입니다. 이 경우 담당자에게 문의하세요 |
| contacts | array | 연락처 목록. 최근 등록순 |
- 정렬은 최초 등록 시각(
created_at) 기준 최신순입니다. 수정(UPSERT)해도 순서는 바뀌지 않습니다. - 목록 조회는 QR 이미지를 새로 만들지 않습니다.
qr_image_url은 등록 시 이미지가 저장되는 주소이므로, 등록 당시 생성에 실패한 건은 링크가 열리지 않을 수 있습니다. 그 연락처를 연락처 조회 API 로 한 번 조회하면 이미지가 만들어집니다.
오류
| HTTP | code | 원인 |
|---|---|---|
| 404 | event_not_found | 행사 없음 또는 키 범위 밖 |
연락처 조회
등록된 연락처 1건을 조회합니다.
GET /api/external/v1/events/{eventId}/contacts/{externalId}
응답 200 — 개인정보는 마스킹됩니다
조회 응답의 이름·이메일·전화번호는 가운데가 가려집니다. API Key 나 토큰이 유출됐을 때 연락처가 대량으로 수집되는 것을 막기 위한 조치입니다. 파트너는 원본을 이미 보유하고 있으므로 실무에는 지장이 없습니다.
{
"id": "con_af2f95b4_REG-001",
"object": "external_contact",
"event_id": "af2f95b402794c8f9289360fc8765b8e",
"external_id": "REG-001",
"name": "J********h",
"email": "jo**@example.com",
"phone": "010-****-5678",
"company": "Acme Inc.",
"position": "CEO",
"extra": {"booth": "A-12"},
"created_at": "2026-09-07T04:50:38Z",
"updated_at": "2026-09-07T04:50:38Z",
"qr_image_url": "https://cdn.connectelly.com/qr/af2f95b402794c8f9289360fc8765b8e/REG-001.png"
}
| 원본 | 조회 응답 |
|---|---|
| john@example.com | jo**@example.com |
| 010-1234-5678 | 010-****-5678 |
| John Smith | J********h |
| 홍길동 | 홍*동 |
회사명·직책·extra 는 마스킹하지 않습니다. 등록·수정(UPSERT) 응답은 방금 보낸 값을 그대로 돌려주므로 마스킹되지 않습니다.
응답 필드
| 필드 | 타입 | 설명 |
|---|---|---|
| id | string | con_ + event_id 앞 8자 + _ + external_id |
| object | string | 항상 external_contact |
| event_id | string | 행사 ID (32자). 경로의 eventId 와 같습니다 |
| external_id | string | 보낸 external_id 그대로 |
| name / email / phone | string | null | 마스킹된 값. 보내지 않았으면 null |
| company / position | string | null | 보낸 값 그대로. 보내지 않았으면 null |
| extra | object | null | 보낸 extra 객체 그대로 (예: {"booth": "A-12"}). 보내지 않았으면 null |
| created_at | string | 최초 등록 시각. ISO 8601 (UTC). 이후 수정해도 바뀌지 않습니다 |
| updated_at | string | 마지막 수정 시각. ISO 8601 (UTC) |
| qr_image_url | string | null | 명찰 인쇄용 QR 이미지 URL (PNG, 600×600px). QR 값은 external_id 원문입니다. 이미지가 없으면 이 조회 때 만듭니다. 이미지를 만들 수 없으면 null |
오류
| HTTP | code | 원인 |
|---|---|---|
| 404 | contact_not_found | 해당 external_id 의 연락처 없음 |
| 404 | event_not_found | 행사 없음 또는 키 범위 밖 |
연락처 삭제
등록된 연락처 1건을 삭제합니다. 참관 취소·중복 등록 정정 시 사용하세요.
DELETE /api/external/v1/events/{eventId}/contacts/{externalId}
curl -X DELETE https://connectelly.com/api/external/v1/events/<eventId>/contacts/REG-001 \
-H "Authorization: Bearer eyJ..."
응답 204
본문 없이 204 No Content 를 반환합니다.
주의
- 삭제 후 같은 QR 을 스캔하면 리드가 캡처되지 않습니다.
- 이미 캡처된 리드는 삭제되지 않습니다 — 앱 유저가 수집한 명함은 그대로 유지됩니다.
- 되돌릴 수 없습니다. 다시 등록하려면 UPSERT 를 호출하세요.
Idempotency-Key헤더를 지원합니다. 같은 키로 재시도하면 첫 응답(204)이 그대로 재생되고, 키 없이 다시 보내면 이미 삭제되어 404contact_not_found입니다.
오류
| HTTP | code | 원인 |
|---|---|---|
| 404 | contact_not_found | 해당 external_id 의 연락처 없음 |
| 409 | idempotency_conflict | 같은 Idempotency-Key 를 다른 요청에 사용 |
| 409 | idempotency_in_progress | 같은 Idempotency-Key 의 첫 요청이 아직 처리 중 |
오류 코드
모든 오류는 Stripe 스타일 포맷으로 응답합니다.
{
"error": {
"type": "invalid_request_error",
"code": "validation_failed",
"message": "external_id must be 100 characters or less.",
"param": "external_id",
"doc_url": null,
"request_id": "req_3f9a1c2b7d4e5f60"
}
}
message 는 개발자 로그용으로 영어로 고정되며 언어 설정에 따라 바뀌지 않습니다. 오류 처리 분기는 code 로 하세요. message 문구에 의존하지 마세요. param 은 문제가 된 필드명이고, request_id 는 문의 시 요청을 추적하는 데 쓰입니다.
| 코드 | HTTP | 의미 |
|---|---|---|
| validation_failed | 400 | 요청 값 검증 실패 (필수 누락·길이 초과·건수 초과 등) |
| invalid_json | 400 | 요청 본문이 올바른 JSON 이 아님 |
| auth_missing | 401 | X-Connectelly-Api-Key 헤더 없음 |
| auth_invalid | 401 | 키 인식 불가 |
| key_expired | 401 | 키 만료 (또는 재발급 유예 종료) |
| key_revoked | 401 | 키 폐기됨 |
| token_missing | 401 | Authorization: Bearer 토큰 없음 |
| token_invalid | 401 | 토큰 위조 또는 만료 |
| ip_not_allowed | 403 | 허용되지 않은 IP |
| event_not_found | 404 | 행사 없음 또는 키 범위 밖 |
| contact_not_found | 404 | 해당 external_id 의 연락처 없음 |
| idempotency_conflict | 409 | 같은 Idempotency-Key 를 다른 요청(메서드·경로·본문)에 사용 — 새 키로 보내세요 |
| idempotency_in_progress | 409 | 같은 Idempotency-Key 의 첫 요청이 아직 처리 중 — 잠시 뒤 같은 요청으로 재시도하세요 |
| payload_too_large | 413 | 요청 본문이 2MB 를 넘음 (Idempotency-Key 를 보낸 요청) — 나눠서 보내세요 |
| rate_limited | 429 | 속도 제한 초과 |
| internal_error | 500 | 서버 오류 |
호출 제한
기본 60 req/min per key per task. Connectelly app-api 는 2~6 개 task 로 오토스케일되므로 실질 허용은 120~360 req/min 사이입니다.
모든 응답에 다음 헤더가 포함됩니다:
X-RateLimit-Limit— task 당 허용치X-RateLimit-Remaining— 현재 창 남은 요청 수Retry-After— 429 시 재시도까지 대기 초
멱등성 (Idempotency)
요청이 타임아웃되면 서버가 이미 처리했는지 알 수 없습니다. Idempotency-Key 헤더를 붙이고 같은 키로 재시도하면 요청은 한 번만 처리되고, 재시도에는 첫 응답이 그대로 돌아옵니다.
헤더
| 헤더 | 필수 | 설명 |
|---|---|---|
Idempotency-Key | 선택 | 출력 가능한 ASCII 1~255자. UUID v4 권장. 새 요청마다 새 키를 만드세요 |
- 빈 값, 255자 초과, 제어 문자가 있으면 400
validation_failed입니다 (param:Idempotency-Key). - 한글 등 ASCII 외 문자는 API 에 닿기 전에 403 으로 거절될 수 있습니다. ASCII 만 쓰세요.
- 적용 대상은
POST /contacts,POST /contacts/batch,DELETE /contacts/{externalId}입니다.GET요청과POST /token은 이 헤더를 무시합니다. - 키는 API Key 별로 구분됩니다. 다른 파트너의 키와 섞이지 않습니다.
- 키는 첫 요청부터 24시간 보관됩니다. 그 뒤 같은 키는 새 요청으로 처리됩니다.
- 이 헤더를 보낸 요청은 본문이 2MB 를 넘으면 413
payload_too_large입니다. 일괄 등록 최대 500건은 이 안에 들어갑니다.
동작
| 경우 | 결과 |
|---|---|
| 키로 보낸 첫 요청 | 정상 처리하고 응답을 저장 (아래 참조) |
| 같은 키, 같은 요청 | 다시 처리하지 않습니다. 첫 응답의 상태코드와 본문을 그대로 반환하고 Idempotent-Replayed: true 헤더를 붙입니다 |
| 같은 키, 다른 요청 | 409 idempotency_conflict — 새 키를 쓰세요 |
| 첫 요청이 아직 처리 중일 때 같은 키 | 409 idempotency_in_progress — 잠시 뒤 같은 요청을 다시 보내세요 |
- "같은 요청"은 메서드·경로·쿼리 문자열·본문이 바이트 단위로 같은 것입니다. 공백이나 필드 순서만 달라도 다른 요청이므로, 처음 보낸 바이트를 그대로 다시 보내세요.
- 재생된 본문의
request_id는 첫 요청의 값이고, 응답 헤더X-Request-Id는 재시도 요청의 새 값입니다. - 재생된 응답도 호출 제한 횟수에 포함됩니다.
HTTP/1.1 200
Content-Type: application/json
Idempotent-Replayed: true
X-Request-Id: req_8b2e4d1f0a9c3e75
저장되는 응답
| 응답 | 같은 키로 재시도하면 |
|---|---|
| 2xx, 그리고 400·404 등 클라이언트 오류 | 저장됨 — 같은 응답이 재생됩니다 |
| 5xx, 429, 408, 409, 401, 403 | 저장 안 됨 — 요청이 다시 처리됩니다 |
400 도 재생되므로, 400 을 받고 고친 요청은 새 키로 보내세요. 고친 본문을 이전 키로 보내면 409 idempotency_conflict 입니다.
예시: 타임아웃 후 재시도
키는 요청마다 한 번 만들고, 재시도할 때는 같은 키를 씁니다. 아래 curl 은 타임아웃·408·429·5xx 에 최대 3번, 같은 키로 재시도합니다. 재시도가 409 idempotency_in_progress 를 받으면 잠시 뒤 같은 요청을 다시 보내세요.
KEY=$(uuidgen)
curl -X POST https://connectelly.com/api/external/v1/events/<eventId>/contacts \
-H "Authorization: Bearer eyJ..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
--max-time 10 --retry 3 \
-d '{ "external_id": "REG-001", "name": "John Smith" }'
변경 이력
- 2026-10-14 —
Idempotency-Key지원 (연락처 등록·일괄·삭제), 연락처 목록 API 문서화, 연락처 조회 응답 필드 보강,extra를 문자열이 아닌 객체로 반환 - v1.0 (2026-09) — 초기 릴리스: Contacts API (단건/일괄/조회/삭제)