← Tài liệu

Open API · Lead

Đẩy lead vào CRM bằng API key

Dành cho lập trình viên bên đối tác. Mỗi lần có khách điền form trên website của bạn, gọi một request HTTP kèm API key — thông tin khách hiện ngay trong NextX CRM và được tự động chia cho nhân viên phụ trách. Không cần OAuth, không cần lấy token định kỳ.

Đường đi của dữ liệu

API key xác định luôn workspace — website đối tác không cần và không được gửi kèm mã công ty.

Website đối tác
Khách điền form → gọi API kèm X-Api-Key
Cổng NextX
Tra khóa → xác định workspace → bỏ khóa khỏi request
CRM
Chống trùng → lưu lead + trường tùy chỉnh → chia nhân viên
Môi trườngBase URLDùng khi nào
Productionhttps://api.nextx.vnChạy thật với khách hàng
Sandboxhttps://api-dev.nextx.vnKiểm thử trong lúc phát triển
Khóa của môi trường nào chỉ dùng được cho môi trường đó. Khóa thử nghiệm không gọi được lên production và ngược lại.
1

Lấy API key

Người quản trị CRM tạo khóa rồi gửi cho bạn.

Khóa chỉ hiện một lần lúc tạo. Ai có khóa là đẩy được dữ liệu vào workspace — giữ như mật khẩu.
2

Thử khóa trước khi viết code

Trả về workspaceId nghĩa là khóa hợp lệ.

GET/api/v1/crm/integrations/ping
Kiểm tra khóa
curl https://api.nextx.vn/api/v1/crm/integrations/ping \
  -H "X-Api-Key: YOUR_API_KEY"
Kết quả
{
  "ok": true,
  "workspaceId": "0198a1b2-c3d4-7890-abcd-ef0123456789",
  "serverTime": "2026-07-30T03:12:56+00:00"
}
3

Lấy danh sách trường

Khỏi phải hỏi qua lại — endpoint này trả đúng mã trường của workspace.

GET/api/v1/crm/integrations/schema
Danh sách trường
curl https://api.nextx.vn/api/v1/crm/integrations/schema \
  -H "X-Api-Key: YOUR_API_KEY"
Kết quả
{
  "standardFields": [
    { "name": "fullName", "label": "Họ tên", "type": "TEXT", "required": true },
    { "name": "phone", "label": "Số điện thoại", "type": "PHONE", "required": false }
  ],
  "customFields": [
    { "name": "Dong_xe", "label": "Dòng xe", "type": "TEXT", "required": false },
    { "name": "Goi_dich_vu", "label": "Gói dịch vụ", "type": "SELECT",
      "required": false, "options": ["Cơ bản", "Nâng cao"] }
  ],
  "sources": ["Website", "Facebook", "Hotline"]
}
Trường tùy chỉnh có thể được công ty thêm hoặc sửa bất cứ lúc nào. Nên gọi lại schema định kỳ thay vì chép cứng danh sách vào code.
4

Đẩy lead

Gọi một lần cho mỗi khách đăng ký. Trả về contactId + mã khách hàng.

POST/api/v1/crm/integrations/leads
Đẩy lead
curl -X POST https://api.nextx.vn/api/v1/crm/integrations/leads \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "fullName": "Nguyễn Văn A",
    "phone": "0912345678",
    "email": "a@example.com",
    "source": "Website",
    "externalId": "WEB-2026-00123",
    "note": "Quan tâm gói cao cấp",
    "city": "Hà Nội",
    "customFields": {
      "Dong_xe": "SUV",
      "Goi_dich_vu": "Nâng cao"
    }
  }'
Kết quả trả về
{
  "contactId": "8f3c1d2e-....",
  "code": "CT00123",
  "isNew": true,
  "matchedBy": null,
  "assigneeId": "b21e...."
}

Các trường gửi lên

Phải có ít nhất số điện thoại hoặc email. Số nhận cả 0912…, 84912…, +84912…

TrườngKiểuBắt buộcRàng buộc / ghi chú
fullNamechuỗiCóTối đa 255 ký tự
phonechuỗiphone / emailTối đa 50 ký tự. Chỉ giữ chữ số rồi đưa về dạng nội địa: 0912…, 84912…, +84912… đều thành 0912…
emailchuỗiphone / emailTối đa 256 ký tự, phải đúng định dạng email. Email KHÔNG dùng để chống trùng
sourcechuỗiKhôngTên nguồn, tối đa 100 ký tự (ví dụ: Website). Trùng tên một nguồn của workspace (không phân biệt hoa/thường) thì gắn nguồn đó; không trùng thì lead vẫn được lưu nhưng không gắn nguồn nào — xem danh sách ở /schema
externalIdchuỗiKhôngMã bản ghi bên bạn, tối đa 100 ký tự. Là lớp chống trùng THỨ HAI, sau số điện thoại (xem mục Chống trùng)
notechuỗiKhôngGhi chú, tối đa 2000 ký tự
ownerIdUUIDKhôngChỉ định người phụ trách; bỏ trống để CRM tự chia. Có truyền thì assigneeId trong kết quả chính là giá trị này
customFieldsđối tượngKhôngKhóa là mã trường lấy từ /schema. Tối đa 50 trường mỗi lần gửi. Sai kiểu hoặc sai lựa chọn → 400
addresschuỗiKhôngĐịa chỉ
citychuỗiKhôngTỉnh / thành phố
wardchuỗiKhôngPhường / xã
dobchuỗiKhôngNgày sinh, dạng yyyy-MM-dd hoặc dd/MM/yyyy. Đọc không ra thì bỏ qua ngày sinh, KHÔNG báo lỗi
genderchuỗiKhôngMale, Female hoặc Other (không phân biệt hoa/thường). Giá trị khác bị bỏ qua
jobTitlechuỗiKhôngChức danh
companyNamechuỗiKhôngTên công ty
createdAtchuỗiKhôngNgày khách bắt đầu có bên bạn (yyyy-MM-dd hoặc dd/MM/yyyy, hiểu theo giờ Việt Nam; hoặc ISO có giờ). CHỈ áp khi tạo mới, và chỉ khi sớm hơn hiện tại và từ năm 1990 trở đi; ngày sai định dạng → 400 INVALID_DATE
branchCodechuỗiKhôngMã chi nhánh nhận lead (không phân biệt hoa/thường). Chỉ áp khi tạo mới, bỏ trống thì CRM tự gắn theo luật của workspace. Mã không có trong workspace → 400 crm.integration.branch_code_not_found — kể cả khi lead đã tồn tại
Phải có ít nhất MỘT trong hai: phone hoặc email.
Quyền của khóa. Khóa CÓ khai scope phải có đúng scope cho từng thao tác: đọc (GET /contacts) = crm.contacts.view, đẩy lead (POST /leads) = crm.contacts.create, cập nhật theo SĐT (PUT /contacts/by-phone) = crm.contacts.edit, và thêm crm.contacts.create khi createIfMissing=true (mặc định). Thiếu scope → 403 crm.integration.scope_required. Khóa không khai scope vẫn dùng được cả nhóm API này. Khóa cũ mang crm.contacts.manage hiện vẫn được chấp nhận thay create/edit nhưng chỉ là tạm thời — hãy cấp khóa mới với mã thật.

Kết quả trả về

TrườngÝ nghĩa
contactIdMã định danh lead trong CRM
codeMã khách hàng hiển thị cho người dùng. Ví dụ trong tài liệu chỉ để minh hoạ — coi là chuỗi hiển thị, đừng phân tích cú pháp
isNewtrue = vừa tạo mới · false = đã tồn tại, CRM trả về bản ghi cũ
matchedBy"external_id" hoặc "phone" khi trùng; null khi tạo mới
assigneeIdChỉ có giá trị khi isNew=true: ownerId bạn gửi, hoặc nhân viên CRM tự chia; null nếu workspace chưa cấu hình tự phân bổ. Khi isNew=false LUÔN là null — không có nghĩa là khách chưa có người phụ trách

Đọc lại và đồng bộ hồ sơ

Ba endpoint còn lại của nhóm tích hợp: lấy lại khách đã đẩy sang, xem chi tiết một khách, và đẩy thay đổi hồ sơ theo số điện thoại.

Danh sách khách hàng

GET/api/v1/crm/integrations/contacts

Chiều đọc của Open API: lấy lại dữ liệu đã đẩy sang, lọc theo khoảng ngày và pagination. Kết quả sắp tăng dần theo mốc đang lọc nên pagination không bị sót khi dữ liệu vẫn đang vào. Mỗi phần tử có id, code, lifecycleStage, customFields (theo mã trường ở /schema), createdAt, updatedAt và các trường hồ sơ như bên dưới.

Tham sốKiểuRàng buộc / ghi chú
fromDatechuỗiMốc bắt đầu, yyyy-MM-dd hoặc dd/MM/yyyy (giờ Việt Nam). Sai định dạng → 400 INVALID_DATE
toDatechuỗiMốc kết thúc, cùng định dạng; chỉ có ngày thì lấy trọn ngày đó
byUpdatedAtbooleanMặc định false = lọc theo ngày tạo; true = lọc theo lần sửa gần nhất (để đồng bộ tăng trưởng)
phonechuỗiLọc theo số điện thoại; được chuẩn hoá giống lúc đẩy lead
page / pageSizesốpage mặc định 1; pageSize mặc định 50, tối thiểu 1, tối đa 200 (lớn hơn bị cắt về 200)
sortByCustomFieldchuỗiMã trường tùy chỉnh kiểu ngày để sắp xếp (kèm sortDescending). Mã không có trong workspace → 400 UNKNOWN_CUSTOM_FIELD
Danh sách khách hàng
curl "https://api.nextx.vn/api/v1/crm/integrations/contacts?fromDate=2026-09-01&toDate=2026-09-30&byUpdatedAt=true&pageSize=100" \
  -H "X-Api-Key: YOUR_API_KEY"
Kết quả
{
  "items": [
    {
      "id": "8f3c1d2e-....",
      "code": "CT00123",
      "fullName": "Nguyễn Văn A",
      "phone": "0912345678",
      "email": "a@example.com",
      "address": null, "ward": null, "city": "Hà Nội",
      "gender": null, "dob": null,
      "jobTitle": null, "companyName": null,
      "source": "Website",
      "note": "Quan tâm gói cao cấp",
      "assigneeId": "b21e....",
      "lifecycleStage": "Lead",
      "customFields": { "Dong_xe": "SUV" },
      "createdAt": "2026-09-12T03:00:00+00:00",
      "updatedAt": "2026-09-20T08:15:00+00:00"
    }
  ],
  "totalCount": 1,
  "page": 1,
  "pageSize": 100,
  "totalPages": 1,
  "hasNextPage": false,
  "hasPreviousPage": false
}

Chi tiết một khách hàng

GET/api/v1/crm/integrations/contacts/{id}

Dùng id lấy từ danh sách hoặc từ kết quả đẩy lead. Trả về đúng bộ trường như phần tử trong danh sách.

Chi tiết một khách hàng
curl https://api.nextx.vn/api/v1/crm/integrations/contacts/8f3c1d2e-.... \
  -H "X-Api-Key: YOUR_API_KEY"
Kết quả
// Trả đúng một đối tượng có bộ trường như phần tử "items" ở trên.
// Không có khách → 404, errorCode = CONTACT_NOT_FOUND.

Cập nhật hồ sơ theo số điện thoại

PUT/api/v1/crm/integrations/contacts/by-phone/{phone}

Dành cho hệ thống đối tác đẩy thay đổi sang CRM. Số điện thoại nằm trên đường dẫn (nhận 0912…, 84912…, +84912…); thân request là các trường dưới đây, tất cả đều tuỳ chọn.

TrườngRàng buộc / ghi chú
fullName, email, address, city, ward, dob, gender, jobTitle, companyNameTrường nào gửi lên thì GHI ĐÈ; trường không gửi giữ nguyên. Lệnh này không bao giờ xoá dữ liệu
noteChỉ điền khi hồ sơ chưa có ghi chú — không ghi đè ghi chú của nhân viên
customFieldsGhi đè các mã trường được gửi (mã lấy từ /schema)
sourceTên nguồn, dùng khi tạo mới hoặc khi đóng dấu externalId
externalIdNÊN gửi kèm. Khác với POST /leads, lệnh này tra externalId TRƯỚC số điện thoại: khách đổi số vẫn về đúng hồ sơ cũ và số mới được ghi đè. Lượt đầu khớp theo số rồi tự đóng dấu mã vào hồ sơ
createIfMissingMặc định true: chưa có khách thì tạo mới (tên bỏ trống thì lấy số điện thoại làm tên). false: chưa có thì trả 404
createdAtChỉ dùng khi lệnh này phải tạo mới; hồ sơ đã có thì ngày tạo giữ nguyên
Cập nhật hồ sơ theo số điện thoại
curl -X PUT https://api.nextx.vn/api/v1/crm/integrations/contacts/by-phone/0912345678 \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "externalId": "ACC-0042",
    "fullName": "Nguyễn Văn A",
    "city": "Đà Nẵng",
    "createIfMissing": true
  }'
Kết quả
{
  "contactId": "8f3c1d2e-....",
  "code": "CT00123",
  "isNew": false,
  "updated": true,
  "matchedBy": "external_id"
}
isNew=true: không tra ra hồ sơ nào nên vừa tạo mới. updated=false: tìm thấy nhưng dữ liệu gửi lên trùng khớp, không ghi gì. matchedBy là "external_id", "phone" hoặc null khi tạo mới. Số trên đường dẫn không đọc được → 400 INVALID_PHONE.

Chống trùng — gửi lại không tạo bản ghi thừa

Đây là luồng máy gọi máy nên gửi lại cùng một lead là chuyện bình thường: người dùng bấm hai lần, hệ thống của bạn retry khi mạng lỗi. CRM tìm khách đã có theo thứ tự sau:

Thứ tựĐiều kiệnKết quả
1Số điện thoại đã có trong workspaceTrả khách cũ, isNew = false, matchedBy = phone
2Không khớp số, nhưng externalId đã gặpTrả khách cũ, isNew = false, matchedBy = external_id
3Không trùngTạo lead mới, isNew = true
Nên gửi externalId bằng mã bản ghi bên bạn, nhưng hiểu đúng vai trò: CRM dò số điện thoại trước, nên externalId chỉ quyết định khi không khớp được theo số — khách đổi số, form không có số, hoặc workspace ở chế độ Tách (bên dưới). Không có cả hai thì mỗi lần gửi là một lead mới. Email không được dùng để chống trùng.
Nếu quản trị viên bật chế độ đẩy dữ liệu Separate (Tách) trong luật chống trùng của workspace, bước theo số điện thoại bị bỏ qua: hai khách chung số vẫn là hai hồ sơ, và externalId là khoá chống trùng duy nhất. Khi số khớp nhiều hơn một khách, CRM trả về khách đầu tiên trong số đó.

Số điện thoại được chuẩn hóa trước khi so khớp: 84912345678 và +84912345678 đều thành 0912345678.

Error code

Lỗi trả về theo chuẩn RFC 7807 — dùng errorCode và title để biết chi tiết.

HTTPNghĩaCách xử lý
200Thành công — kể cả khi trùng (isNew=false)Không cần làm gì
400Dữ liệu không đọc được hoặc giá trị không hợp lệ: JSON hỏng, ngày sai định dạng (INVALID_DATE), mã chi nhánh lạ, mã trường tùy chỉnh sai, số điện thoại trên đường dẫn không hợp lệ (INVALID_PHONE)Đọc errorCode và title trong phản hồi, sửa dữ liệu rồi gửi lại
401Thiếu hoặc sai API keyKiểm tra header X-Api-Key; khóa có thể đã bị thu hồi
403Khóa CÓ khai scope nhưng thiếu scope cho thao tác này (crm.integration.scope_required)Cấp thêm scope: đọc = crm.contacts.view, đẩy lead = crm.contacts.create, cập nhật theo SĐT = crm.contacts.edit (kèm crm.contacts.create nếu createIfMissing=true, là mặc định). Khóa không khai scope vẫn chạy cả ba
404Không có khách tương ứng (CONTACT_NOT_FOUND) — GET theo id, hoặc PUT theo SĐT khi createIfMissing=falseKiểm tra id / số điện thoại; muốn tự tạo khi chưa có thì bỏ createIfMissing=false
409CRM chưa tra được danh sách chi nhánh để kiểm branchCode (crm.integration.branch_lookup_failed)Retry sau; nếu lặp lại, báo đội NextX kèm traceId
422Không qua được kiểm tra ràng buộcXem errors (đối tượng theo tên trường, mỗi trường là danh sách thông báo) để biết trường nào sai
429Gọi quá nhiều trong thời gian ngắnChờ rồi retry, lùi dần: 1s, 2s, 4s
5xxLỗi phía NextXRetry sau; nếu lặp lại, báo đội NextX kèm traceId

Lỗi do CRM trả về theo chuẩn RFC 7807 (application/problem+json) và luôn kèm traceId — chuỗi 32 ký tự hex. Hãy ghi log giá trị này để đội NextX tra đúng request khi bạn cần hỗ trợ. Dùng errorCode (cho máy đọc) và title (cho người đọc); trường detail chỉ xuất hiện ở môi trường không phải production nên đừng dựa vào nó.

ProblemDetails
{
  "type": "https://nextx.vn/errors/validation-failed",
  "title": "Validation failed: ...",
  "status": 422,
  "errorCode": "common.validation_failed",
  "traceId": "08b16a93df7c4f44ad2031b086fb0351",
  "errors": {
    "FullName": ["Họ tên không được để trống"],
    "": ["Phải có ít nhất số điện thoại hoặc email"]
  }
}

Ví dụ mã nguồn

Chép về đổi khóa và tên trường là chạy được.

JavaScript (Node.js)
const res = await fetch(
  'https://api.nextx.vn/api/v1/crm/integrations/leads',
  {
    method: 'POST',
    headers: {
      'X-Api-Key': process.env.NEXTX_API_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      fullName: form.name,
      phone: form.phone,
      email: form.email,
      source: 'Website',
      externalId: `WEB-${record.id}`,
    }),
  }
);

const data = await res.json();
if (!res.ok) throw new Error(`${data.errorCode}: ${data.title} (traceId ${data.traceId})`);
console.log(data.contactId, data.isNew);
PHP
$payload = [
  'fullName'   => $name,
  'phone'      => $phone,
  'email'      => $email,
  'source'     => 'Website',
  'externalId' => 'WEB-' . $recordId,
];

$ch = curl_init('https://api.nextx.vn/api/v1/crm/integrations/leads');
curl_setopt_array($ch, [
  CURLOPT_POST           => true,
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER     => [
    'X-Api-Key: ' . getenv('NEXTX_API_KEY'),
    'Content-Type: application/json',
  ],
  CURLOPT_POSTFIELDS     => json_encode($payload, JSON_UNESCAPED_UNICODE),
]);

$response = curl_exec($ch);
$status   = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

Trước khi chạy thật

Đi hết danh sách này trước khi bật cho khách thật.

  • Gọi /ping thành công, workspaceId đúng công ty cần nhận dữ liệu.
  • Gọi /schema và đối chiếu mã trường tùy chỉnh với dữ liệu bạn định gửi.
  • Gửi thử một lead trên sandbox, xác nhận nhân viên CRM nhìn thấy.
  • Gửi lại đúng lead đó lần hai, xác nhận nhận isNew=false.
  • API key nằm trong environment variable phía máy chủ, không lộ ra trình duyệt hay repo code.
  • Có ghi log traceId khi gặp lỗi.
  • Có cơ chế retry khi gặp 429 hoặc 5xx.