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.
| Môi trường | Base URL | Dùng khi nào |
|---|---|---|
| Production | https://api.nextx.vn | Chạy thật với khách hàng |
| Sandbox | https://api-dev.nextx.vn | Kiểm thử trong lúc phát triển |
Lấy API key
Người quản trị CRM tạo khóa rồi gửi cho bạn.
Thử khóa trước khi viết code
Trả về workspaceId nghĩa là khóa hợp lệ.
/api/v1/crm/integrations/pingcurl https://api.nextx.vn/api/v1/crm/integrations/ping \
-H "X-Api-Key: YOUR_API_KEY"{
"ok": true,
"workspaceId": "0198a1b2-c3d4-7890-abcd-ef0123456789",
"serverTime": "2026-07-30T03:12:56+00:00"
}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.
/api/v1/crm/integrations/schemacurl https://api.nextx.vn/api/v1/crm/integrations/schema \
-H "X-Api-Key: YOUR_API_KEY"{
"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"]
}Đẩy lead
Gọi một lần cho mỗi khách đăng ký. Trả về contactId + mã khách hàng.
/api/v1/crm/integrations/leadscurl -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"
}
}'{
"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ường | Kiểu | Bắt buộc | Ràng buộc / ghi chú |
|---|---|---|---|
fullName | chuỗi | Có | Tối đa 255 ký tự |
phone | chuỗi | phone / email | Tố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… |
email | chuỗi | phone / email | Tối đa 256 ký tự, phải đúng định dạng email. Email KHÔNG dùng để chống trùng |
source | chuỗi | Không | Tê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 |
externalId | chuỗi | Không | Mã 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) |
note | chuỗi | Không | Ghi chú, tối đa 2000 ký tự |
ownerId | UUID | Không | Chỉ đị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ượng | Không | Khó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 |
address | chuỗi | Không | Địa chỉ |
city | chuỗi | Không | Tỉnh / thành phố |
ward | chuỗi | Không | Phường / xã |
dob | chuỗi | Không | Ngà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 |
gender | chuỗi | Không | Male, Female hoặc Other (không phân biệt hoa/thường). Giá trị khác bị bỏ qua |
jobTitle | chuỗi | Không | Chức danh |
companyName | chuỗi | Không | Tên công ty |
createdAt | chuỗi | Không | Ngà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 |
branchCode | chuỗi | Không | Mã 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 |
phone hoặc email.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 |
|---|---|
contactId | Mã định danh lead trong CRM |
code | Mã 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 |
isNew | true = 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 |
assigneeId | Chỉ 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
/api/v1/crm/integrations/contactsChiề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ểu | Ràng buộc / ghi chú |
|---|---|---|
fromDate | chuỗi | Mốc bắt đầu, yyyy-MM-dd hoặc dd/MM/yyyy (giờ Việt Nam). Sai định dạng → 400 INVALID_DATE |
toDate | chuỗi | Mốc kết thúc, cùng định dạng; chỉ có ngày thì lấy trọn ngày đó |
byUpdatedAt | boolean | Mặ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) |
phone | chuỗi | Lọc theo số điện thoại; được chuẩn hoá giống lúc đẩy lead |
page / pageSize | số | 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) |
sortByCustomField | chuỗi | Mã 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 |
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"{
"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
/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.
curl https://api.nextx.vn/api/v1/crm/integrations/contacts/8f3c1d2e-.... \
-H "X-Api-Key: YOUR_API_KEY"// 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
/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ường | Ràng buộc / ghi chú |
|---|---|
fullName, email, address, city, ward, dob, gender, jobTitle, companyName | Trườ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 |
note | Chỉ điền khi hồ sơ chưa có ghi chú — không ghi đè ghi chú của nhân viên |
customFields | Ghi đè các mã trường được gửi (mã lấy từ /schema) |
source | Tên nguồn, dùng khi tạo mới hoặc khi đóng dấu externalId |
externalId | NÊ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ơ |
createIfMissing | Mặ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 |
createdAt | Chỉ dùng khi lệnh này phải tạo mới; hồ sơ đã có thì ngày tạo giữ nguyên |
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
}'{
"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ện | Kết quả |
|---|---|---|
| 1 | Số điện thoại đã có trong workspace | Trả khách cũ, isNew = false, matchedBy = phone |
| 2 | Không khớp số, nhưng externalId đã gặp | Trả khách cũ, isNew = false, matchedBy = external_id |
| 3 | Không trùng | Tạo lead mới, isNew = true |
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.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.
| HTTP | Nghĩa | Cách xử lý |
|---|---|---|
| 200 | Thành công — kể cả khi trùng (isNew=false) | Không cần làm gì |
| 400 | Dữ 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 |
| 401 | Thiếu hoặc sai API key | Kiểm tra header X-Api-Key; khóa có thể đã bị thu hồi |
| 403 | Khó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 |
| 404 | Không có khách tương ứng (CONTACT_NOT_FOUND) — GET theo id, hoặc PUT theo SĐT khi createIfMissing=false | Kiểm tra id / số điện thoại; muốn tự tạo khi chưa có thì bỏ createIfMissing=false |
| 409 | CRM 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 |
| 422 | Không qua được kiểm tra ràng buộc | Xem 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 |
| 429 | Gọi quá nhiều trong thời gian ngắn | Chờ rồi retry, lùi dần: 1s, 2s, 4s |
| 5xx | Lỗi phía NextX | Retry 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ó.
{
"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.
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);$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
/pingthành công,workspaceIdđúng công ty cần nhận dữ liệu. - Gọi
/schemavà đố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
traceIdkhi gặp lỗi. - Có cơ chế retry khi gặp 429 hoặc 5xx.