Tài liệu API

Mọi thứ bạn cần để kết nối hệ thống đăng ký với Connectelly.

Bắt đầu nhanh

Connectelly External API sử dụng xác thực hai bước. Đổi API key lấy một access token có thời hạn ngắn, sau đó gọi API bằng token đó.

  1. Đăng ký làm đối tác và nhận API key — quản trị viên chính (master) cấp key sau khi ký hợp đồng.
  2. Lấy access token — POST /api/external/v1/token
  3. Gọi API bằng token — Authorization: Bearer <access_token>
# 1) Lấy token (API key chỉ dùng ở bước này)
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 nhận được ở đây

# 2) Gửi liên hệ bằng token vừa nhận
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"}'

Phản hồi 200 nghĩa là đã lưu thành công. Khi người dùng ứng dụng quét QR của REG-001, lead sẽ được thu thập tự động.

Xác thực

Xác thực hai bước

API key dài hạn chỉ dùng để lấy token; mọi lệnh gọi API thực tế đều dùng access token ngắn hạn. Key không đi kèm lưu lượng thông thường nên không thể bị lộ qua log máy chủ hay proxy của đối tác.

API key

  • Định dạng: ck_live_<32 url-safe base64>
  • Chỉ gửi qua header X-Connectelly-Api-Key tại POST /api/external/v1/token
  • Cấp theo từng sự kiện. Không dùng được cho sự kiện khác (404 event_not_found)
  • Chỉ dùng phía máy chủ. Không nhúng vào mã phía client hoặc đưa lên kho mã công khai

Access Token

  • Hiệu lực 30 phút (1800 giây). Hãy lấy token mới khi hết hạn.
  • Gửi Authorization: Bearer <access_token> trong mọi lệnh gọi API.
  • Khi key bị thu hồi, các token đã cấp cũng mất hiệu lực ngay lập tức.

Khi key bị lộ

Hãy báo ngay cho quản trị viên để thu hồi key. Ngay khi thu hồi, key đó và mọi token được cấp từ key đó đều bị chặn. Khi được cấp key mới, key cũ vẫn còn hiệu lực trong 24 giờ ân hạn, nên bạn có thể thay key mà không bị gián đoạn.

Giới hạn IP (khuyến nghị)

Khi đăng ký các IP/CIDR được phép, token chỉ có thể được cấp từ những địa chỉ đó. Dù key bị lộ cũng không thể dùng ở nơi khác. Hãy liên hệ quản trị viên để thiết lập.

Token API

Đổi API key lấy access token ngắn hạn. Đây là endpoint duy nhất nhận API key trong header; mọi API còn lại đều gọi bằng token nhận được ở đây.

Yêu cầu

POST /api/external/v1/token

HeaderBắt buộcMô tả
X-Connectelly-Api-KeyCóAPI key đã được cấp (ck_live_...)

Không cần body cho yêu cầu.

curl -X POST https://connectelly.com/api/external/v1/token \
  -H "X-Connectelly-Api-Key: ck_live_..."

Phản hồi 200

{
  "access_token": "eyJhbGciOiJIUzM4NCJ9...",
  "token_type": "Bearer",
  "expires_in": 1800,
  "expires_at": "2026-09-07T05:18:35Z",
  "event_id": "af2f95b402794c8f9289360fc8765b8e"
}
TrườngKiểuMô tả
access_tokenstringDùng trong header Authorization của mọi yêu cầu sau đó
token_typestringLuôn là Bearer
expires_inintegerThời gian hiệu lực còn lại (giây). Mặc định 1800 = 30 phút
expires_atstringThời điểm hết hạn (ISO-8601 UTC)
event_idstringID sự kiện gắn với key này. Dùng nguyên giá trị này cho {eventId} trong đường dẫn của mọi endpoint khác

Lấy eventId ở đâu?

Mỗi API key được cấp gắn với một sự kiện duy nhất, nên bạn không cần tự quản lý eventId. Có hai cách để biết giá trị này:

  • event_id trong phản hồi token — lấy trực tiếp từ phản hồi ở trên. Nên dùng giá trị này thay vì hardcode.
  • Thông báo khi cấp key — người phụ trách sẽ cung cấp kèm khi giao API key.

Dùng eventId của sự kiện khác sẽ trả về 404 event_not_found.

Lỗi

HTTPcodeNguyên nhân
401auth_missingThiếu header
401auth_invalidKhông nhận dạng được key
401key_expiredKey đã hết hạn (hoặc hết thời gian ân hạn khi cấp lại)
401key_revokedKey đã bị thu hồi
403ip_not_allowedIP không được phép

Mẹo vận hành

  • Lưu token vào bộ nhớ đệm thay vì lấy mới cho mỗi yêu cầu. Nên làm mới trước khi hết hạn 1–2 phút.
  • Khi nhận 401 token_invalid, hãy lấy token mới và thử lại đúng 1 lần.
  • Chỉ giữ token trong bộ nhớ máy chủ, không ghi vào log hoặc URL.

Tạo / Cập nhật liên hệ

Tạo hoặc cập nhật một liên hệ. Nếu external_id đã tồn tại thì ghi đè (UPSERT). Gọi API này ngay khi đăng ký để lead được thu thập tự động khi ứng dụng quét QR.

POST /api/external/v1/events/{eventId}/contacts

Header của yêu cầu

HeaderBắt buộcMô tả
AuthorizationCóBearer <access_token>
Content-TypeCóapplication/json
Idempotency-KeyKhôngThử lại với cùng key sẽ nhận phản hồi đầu tiên thay vì xử lý lại. Xem mục Idempotency

Body của yêu cầu

TrườngKiểuBắt buộcMô tả
external_idstringCóID duy nhất trong hệ thống đăng ký, tối đa 100 ký tự. Phải khớp với giá trị QR
namestringKhôngHọ tên, tối đa 200 ký tự
emailstringKhôngEmail. Mã hóa AES-256-GCM khi lưu
phonestringKhôngSố điện thoại. Mã hóa khi lưu
companystringKhôngTên công ty, tối đa 200 ký tự
positionstringKhôngChức danh, tối đa 100 ký tự
extraobjectKhôngJSON tự do cho các trường mở rộng của đối tác
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" }
  }'

Phản hồi 200

Trả về nguyên giá trị bạn vừa gửi (để xác nhận đã ghi). Các trường có cùng định dạng với Lấy liên hệ, riêng qr_image_url trong phản hồi này luôn là null — hãy lấy URL ảnh QR qua Lấy liên hệ hoặc Danh sách liên hệ.

{
  "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
}

Lỗi

HTTPcodeNguyên nhân
400validation_failedThiếu external_id hoặc trường vượt độ dài
400invalid_jsonNội dung yêu cầu không phải JSON hợp lệ
401token_missing / token_invalidThiếu token / token giả mạo hoặc hết hạn
404event_not_foundSự kiện không tồn tại hoặc nằm ngoài phạm vi của key
409idempotency_conflictCùng Idempotency-Key được dùng cho một yêu cầu khác
409idempotency_in_progressYêu cầu đầu tiên với Idempotency-Key này vẫn đang được xử lý
429rate_limitedVượt giới hạn tốc độ

Tải lên hàng loạt

UPSERT tối đa 500 liên hệ trong một lần gọi. Dùng cho lần nạp dữ liệu ban đầu hoặc đồng bộ ban đêm.

POST /api/external/v1/events/{eventId}/contacts/batch

Body của yêu cầu

TrườngKiểuBắt buộcMô tả
contactsarrayCóMảng liên hệ, 1–500 mục. Mỗi mục có cùng định dạng với API tạo một liên hệ
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" }
    ]
  }'

Phản hồi 200

Mục thứ hai trong ví dụ không có external_id, nên chỉ mục đó thất bại, các mục còn lại vẫn được lưu.

{
  "total": 2,
  "created": 1,
  "updated": 0,
  "failed": 1,
  "errors": [
    {
      "external_id": null,
      "code": "validation_failed",
      "message": "external_id is required.",
      "param": "external_id"
    }
  ]
}
TrườngKiểuMô tả
totalintegerTổng số mục trong yêu cầu
createdintegerSố liên hệ mới được tạo
updatedintegerSố liên hệ hiện có bị ghi đè
failedintegerSố mục không được lưu
errorsarrayChỉ gồm các mục thất bại (external_id, code, message, param). Mảng rỗng nếu tất cả thành công

Lưu ý

  • Mảng rỗng hoặc vượt quá 500 mục sẽ khiến toàn bộ yêu cầu trả về 400 validation_failed.
  • Cho phép thành công một phần. Dù có mục thất bại, các mục hợp lệ vẫn được lưu. Chỉ cần sửa các mục trong errors rồi gửi lại; vì là UPSERT nên không tạo bản trùng.
  • Khi nạp số lượng lớn, hãy giãn cách các lần gọi theo mục Giới hạn tốc độ để tránh 429.
  • Hỗ trợ header Idempotency-Key — thử lại lần gọi bị hết thời gian chờ với cùng key thì yêu cầu chỉ được xử lý một lần. Xem mục Idempotency.

Danh sách liên hệ

Trả về toàn bộ liên hệ đã lưu cho sự kiện này trong một lần gọi. Dùng để đối chiếu dữ liệu đã gửi, hoặc lấy hàng loạt ảnh QR để in thẻ tên.

GET /api/external/v1/events/{eventId}/contacts

curl https://connectelly.com/api/external/v1/events/<eventId>/contacts \
  -H "Authorization: Bearer eyJ..."

Phản hồi 200

Không có tham số truy vấn và không phân trang. Mỗi phần tử trong contacts có cùng các trường với phản hồi Lấy liên hệ; họ tên, email và số điện thoại cũng được che theo cùng cách.

{
  "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"
    }
  ]
}
TrườngKiểuMô tả
totalintegerTổng số liên hệ đã lưu cho sự kiện này
returnedintegerSố liên hệ trong phản hồi này (tối đa 10.000)
truncatedbooleantrue nghĩa là đã chạm giới hạn 10.000 và chỉ 10.000 liên hệ mới nhất được trả về. Khi đó hãy liên hệ với chúng tôi
contactsarrayDanh sách liên hệ, mới đăng ký nhất xếp trước
  • Sắp xếp theo thời điểm đăng ký lần đầu (created_at), mới nhất trước. Cập nhật (UPSERT) không làm thay đổi thứ tự.
  • Lấy danh sách không tạo ảnh QR mới. qr_image_url là địa chỉ lưu ảnh khi đăng ký, nên liên hệ bị lỗi tạo ảnh lúc đăng ký có thể không mở được liên kết. Gọi Lấy liên hệ cho liên hệ đó một lần là ảnh sẽ được tạo.

Lỗi

HTTPcodeNguyên nhân
404event_not_foundSự kiện không tồn tại hoặc nằm ngoài phạm vi của key

Lấy liên hệ

Lấy một liên hệ đã đăng ký.

GET /api/external/v1/events/{eventId}/contacts/{externalId}

Phản hồi 200 — thông tin cá nhân được che

Họ tên, email và số điện thoại trong phản hồi bị che phần giữa. Biện pháp này ngăn việc thu thập liên hệ hàng loạt khi API key hoặc token bị lộ. Đối tác đã có dữ liệu gốc nên không ảnh hưởng đến công việc hằng ngày.

{
  "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"
}
Giá trị gốcPhản hồi GET
john@example.comjo**@example.com
010-1234-5678010-****-5678
John SmithJ********h
홍길동홍*동

Tên công ty, chức danh và extra không bị che. Phản hồi tạo / cập nhật (UPSERT) trả về nguyên giá trị bạn vừa gửi nên không bị che.

Các trường trong phản hồi

TrườngKiểuMô tả
idstringcon_ + 8 ký tự đầu của event_id + _ + external_id
objectstringLuôn là external_contact
event_idstringID sự kiện (32 ký tự), giống eventId trong đường dẫn
external_idstringNguyên external_id bạn đã gửi
name / email / phonestring | nullGiá trị đã che. null nếu không gửi
company / positionstring | nullNguyên giá trị đã gửi. null nếu không gửi
extraobject | nullTrả về nguyên đối tượng extra bạn đã gửi (ví dụ: {"booth": "A-12"}). null nếu không gửi
created_atstringThời điểm đăng ký lần đầu, ISO 8601 (UTC). Không đổi khi cập nhật sau đó
updated_atstringThời điểm cập nhật gần nhất, ISO 8601 (UTC)
qr_image_urlstring | nullURL ảnh QR để in thẻ tên (PNG, 600×600px). Giá trị QR là nguyên external_id. Nếu chưa có ảnh, lần gọi này sẽ tạo ảnh. null nếu không tạo được ảnh

Lỗi

HTTPcodeNguyên nhân
404contact_not_foundKhông có liên hệ với external_id này
404event_not_foundSự kiện không tồn tại hoặc nằm ngoài phạm vi của key

Xoá liên hệ

Xoá một liên hệ đã đăng ký. Dùng khi khách huỷ tham quan hoặc để sửa đăng ký trùng.

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..."

Phản hồi 204

Trả về 204 No Content với body rỗng.

Lưu ý

  • Sau khi xoá, quét lại QR đó sẽ không thu thập lead.
  • Các lead đã thu thập không bị xoá — danh thiếp mà người dùng ứng dụng đã thu thập vẫn được giữ nguyên.
  • Không thể hoàn tác. Để đăng ký lại, hãy gọi API UPSERT.
  • Hỗ trợ header Idempotency-Key. Thử lại với cùng key sẽ phát lại phản hồi đầu tiên (204); gửi lại không kèm key sẽ nhận 404 contact_not_found vì liên hệ đã bị xoá.

Lỗi

HTTPcodeNguyên nhân
404contact_not_foundKhông có liên hệ với external_id này
409idempotency_conflictCùng Idempotency-Key được dùng cho một yêu cầu khác
409idempotency_in_progressYêu cầu đầu tiên với Idempotency-Key này vẫn đang được xử lý

Mã lỗi

Mọi lỗi đều được trả về theo định dạng kiểu 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 dành cho log của lập trình viên và luôn bằng tiếng Anh, không thay đổi theo cài đặt ngôn ngữ. Hãy rẽ nhánh xử lý lỗi theo code, đừng dựa vào nội dung message. param là tên trường gây lỗi, còn request_id giúp truy vết yêu cầu khi bạn liên hệ hỗ trợ.

MãHTTPÝ nghĩa
validation_failed400Dữ liệu yêu cầu không hợp lệ (thiếu trường bắt buộc, vượt độ dài, vượt số lượng, v.v.)
invalid_json400Nội dung yêu cầu không phải JSON hợp lệ
auth_missing401Thiếu header X-Connectelly-Api-Key
auth_invalid401Không nhận dạng được key
key_expired401Key đã hết hạn (hoặc hết thời gian ân hạn khi cấp lại)
key_revoked401Key đã bị thu hồi
token_missing401Thiếu token Authorization: Bearer
token_invalid401Token không hợp lệ hoặc đã hết hạn
ip_not_allowed403IP không được phép
event_not_found404Sự kiện không tồn tại hoặc nằm ngoài phạm vi của key
contact_not_found404Không có liên hệ với external_id này
idempotency_conflict409Cùng Idempotency-Key được dùng cho một yêu cầu khác (phương thức, đường dẫn hoặc body) — hãy dùng key mới
idempotency_in_progress409Yêu cầu đầu tiên với Idempotency-Key này vẫn đang được xử lý — đợi một lát rồi thử lại đúng yêu cầu đó
payload_too_large413Nội dung yêu cầu vượt quá 2 MB (yêu cầu có Idempotency-Key) — hãy chia nhỏ rồi gửi
rate_limited429Vượt giới hạn tốc độ
internal_error500Lỗi máy chủ

Giới hạn tốc độ

Mặc định 60 req/min per key per task. Connectelly app-api tự động co giãn trong khoảng 2–6 task, nên mức cho phép thực tế là 120–360 req/min.

Mọi phản hồi đều có các header sau:

  • X-RateLimit-Limit — mức cho phép trên mỗi task
  • X-RateLimit-Remaining — số yêu cầu còn lại trong cửa sổ hiện tại
  • Retry-After — số giây cần chờ trước khi thử lại khi nhận 429

Idempotency

Khi một yêu cầu bị hết thời gian chờ, bạn không biết máy chủ đã xử lý hay chưa. Hãy gửi header Idempotency-Key và thử lại với cùng key: yêu cầu chỉ được xử lý một lần, và mỗi lần thử lại đều nhận về phản hồi đầu tiên.

Header

HeaderBắt buộcMô tả
Idempotency-KeyKhông1–255 ký tự ASCII in được. Khuyến nghị UUID v4. Tạo key mới cho mỗi yêu cầu mới
  • Key rỗng, dài hơn 255 ký tự hoặc chứa ký tự điều khiển sẽ trả về 400 validation_failed (param: Idempotency-Key).
  • Ký tự ngoài ASCII (ví dụ tiếng Việt có dấu) có thể bị từ chối với 403 trước khi đến API. Chỉ dùng ASCII.
  • Áp dụng cho POST /contacts, POST /contacts/batch và DELETE /contacts/{externalId}. Yêu cầu GET và POST /token bỏ qua header này.
  • Key được phân tách theo API Key — không bao giờ trùng với key của đối tác khác.
  • Key được lưu 24 giờ kể từ yêu cầu đầu tiên. Sau đó, cùng key được xử lý như một yêu cầu mới.
  • Với header này, nội dung yêu cầu vượt quá 2 MB sẽ trả về 413 payload_too_large. Một lô tối đa 500 liên hệ nằm trong giới hạn này.

Cách hoạt động

Trường hợpKết quả
Yêu cầu đầu tiên với một keyXử lý bình thường; phản hồi được lưu (xem bên dưới)
Cùng key, cùng yêu cầuKhông xử lý lại. Trả về nguyên mã trạng thái và body của phản hồi đầu tiên, kèm header Idempotent-Replayed: true
Cùng key, yêu cầu khác409 idempotency_conflict — hãy dùng key mới
Cùng key khi yêu cầu đầu tiên vẫn đang xử lý409 idempotency_in_progress — đợi một lát rồi gửi lại đúng yêu cầu đó
  • "Cùng yêu cầu" nghĩa là cùng phương thức, đường dẫn, query string và body, giống nhau từng byte. Chỉ khác khoảng trắng hoặc thứ tự trường cũng là yêu cầu khác, vì vậy hãy gửi lại đúng các byte đã gửi lần đầu.
  • Body được phát lại giữ request_id của yêu cầu đầu tiên; header phản hồi X-Request-Id mang giá trị mới của lần thử lại.
  • Phản hồi được phát lại cũng được tính vào giới hạn tốc độ.
HTTP/1.1 200
Content-Type: application/json
Idempotent-Replayed: true
X-Request-Id: req_8b2e4d1f0a9c3e75

Phản hồi nào được lưu

Phản hồiThử lại với cùng key
2xx, và lỗi phía client như 400, 404Được lưu — phát lại cùng phản hồi
5xx, 429, 408, 409, 401, 403Không lưu — yêu cầu được xử lý lại

Vì 400 cũng được phát lại, sau khi sửa một yêu cầu bị 400, hãy dùng key mới. Gửi body đã sửa với key cũ sẽ nhận 409 idempotency_conflict.

Ví dụ: thử lại sau khi hết thời gian chờ

Tạo key một lần cho mỗi yêu cầu và dùng lại key đó ở mọi lần thử lại. Lệnh curl dưới đây thử lại tối đa 3 lần khi hết thời gian chờ, 408, 429 và 5xx, luôn gửi cùng key. Nếu lần thử lại nhận 409 idempotency_in_progress, hãy đợi một lát rồi gửi lại đúng yêu cầu đó.

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" }'

Nhật ký thay đổi

  • 2026-10-14 — Hỗ trợ Idempotency-Key (tạo / hàng loạt / xoá liên hệ), bổ sung tài liệu API danh sách liên hệ, thêm trường cho phản hồi lấy liên hệ, extra được trả về dưới dạng đối tượng thay vì chuỗi
  • v1.0 (2026-09) — Phát hành đầu tiên: Contacts API (đơn lẻ / hàng loạt / lấy / xoá)