Chuyển đến nội dung chính

API: POST /track/open

Bạn sẽ học: Toàn bộ hợp đồng của endpoint POST /api/v1/track/open — cách xác thực, các trường request, server chọn đường xử lý ra sao, hình dạng response, và ý nghĩa từng giá trị matchMethod / missReason.

Endpoint & môi trường

POST https://api.li2.ai/api/v1/track/open
Đây là base URL production duy nhất — dùng chung cho mọi organization, không có host sandbox/staging riêng. Bạn test ngay trên production bằng custom domain đã verify của mình và một bản cài thật từ store (TestFlight/internal-test) — xem Kiểm thử trên production. Endpoint đi qua các middleware: CORS, rate limit (1000 request/phút/IP), và AuthMobileApikeyMiddleware.

Xác thực

Gửi publishable key trong header:
X-Li2-Key: li2_pk_xxxxxxxxxxxxxxxxxxxx
  • Lấy publishable key ở đâu: dashboard → Settings → Analytics → Publishable Key (dạng li2_pk_...). Key được tạo tự động khi bật Analytics; chi tiết tại Thiết lập Conversion Tracking.
  • Fallback cho caller không đặt được custom header (ví dụ sendBeacon): tham số query ?li2_key=....
  • Không dùng server API key (X-Li2-API-Key) cho endpoint này — sẽ trả về 401. Endpoint mobile chỉ chấp nhận key dạng li2_pk_*.
Vì sao publishable key an toàn khi nhúng trong app: với request từ web (có Origin/Referer), server kiểm tra origin theo whitelist của key. Với app native (không có Origin/Referer), server bỏ qua bước origin và kiểm tra phía server rằng domain được track thuộc đúng organization của key. Key không thể dùng để track domain tùy ý hay truy cập dữ liệu org khác.

Request

Request
{
  "deepLink": "https://your-domain.com/slug?li2_cid=<16-ký-tự-base62>",
  "li2Domains": ["your-domain.com"],
  "clipboardStatus": "read | empty | denied | optout",
  "installReferrer": "<chuỗi-referrer-thô-từ-Play>"
}
TrườngBắt buộcGhi chú
deepLinkCó điều kiệnURL hợp lệ, có host và path slug khác rỗng
li2DomainsCó điều kiệnMảng hostname (không kèm scheme/path) mà app được entitle. Thường chỉ một; gửi nhiều nếu app trải trên nhiều custom domain. Khi clipboard rỗng/từ chối, trường này cho server biết domain/org nào để ghi nhận deferred_miss đúng chỗ.
clipboardStatusKhôngChỉ nhận read, empty, denied, optout; giá trị khác → 400
installReferrerCó điều kiệnChuỗi referrer thô từ Google Play
Phải có ít nhất một trong deepLink, li2Domains, hoặc installReferrer.

Server chọn đường xử lý như thế nào

Điều kiệnĐường xử lýÝ nghĩa
installReferrerAndroid deferredGiải mã referrer, lấy li2_cid, đối chiếu deferred
deepLink li2_cidiOS clipboard deferredĐối chiếu deferred theo clipboard token
deepLink, khôngli2_cidImmediateApp đã cài, link mở bình thường
Chỉ có li2Domains (kèm clipboardStatus)Ghi nhận missiOS báo clipboard rỗng / từ chối / bỏ qua → ghi deferred_miss
Trước khi xử lý, server kiểm tra org có gói Pro (deep_link_attribution) và suy ra platform (ios/android) từ User-Agent.

Response

Response
{
  "clickId": "1a2b3c4d5e6f7g8h",
  "link": {
    "id": "<touch-point-uuid>",
    "domain": "your-domain.com",
    "key": "slug",
    "url": "https://destination.example.com/path"
  },
  "matchMethod": "universal_link | app_link | clipboard | install_referrer",
  "missReason": "no_candidate | ...",
  "platform": "ios | android"
}
  • Khi khớp: link được điền, missReason bị bỏ qua.
  • Khi trượt (deferred không đối chiếu được): linknull, missReason được điền.
  • clickId luôn có: dùng id của click gốc khi khớp, hoặc một id ngẫu nhiên khi trượt (đồng thời ghi một dòng deferred_miss).
  • link.idUUID của touch point (cấu hình link trong Li2), không phải id của short link hiển thị hay của click. Điều hướng người dùng bằng link.url; link.key là slug, link.domain là host.

Ví dụ theo từng luồng

Mỗi tab là một lệnh curl chạy được (thay your-domain.comli2_pk_... bằng giá trị của bạn) kèm response mẫu.
platformmatchMethod suy ra từ User-Agent. Server đọc UA của request để phân loại ios/android. Một lệnh curl mặc định (UA curl/8.x) khớp không nền tảng nào → trả về platform: nullmatchMethod rỗng. Vì vậy các ví dụ dưới đây đều kèm header -A giả lập UA thiết bị để response khớp như minh họa. Từ app thật, UA của URLSession/okhttp đã chứa sẵn dấu hiệu nền tảng nên bạn không cần làm gì thêm.
App đã cài, mở qua Universal Link / App Link — chỉ gửi deepLink, khôngli2_cid:
Request
curl -X POST https://api.li2.ai/api/v1/track/open \
  -H "Content-Type: application/json" \
  -H "X-Li2-Key: li2_pk_xxxxxxxxxxxxxxxxxxxx" \
  -A "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)" \
  -d '{ "deepLink": "https://your-domain.com/sale" }'
Response · 200
{
  "clickId": "1a2b3c4d5e6f7g8h",
  "link": {
    "id": "0e7c9b12-...",
    "domain": "your-domain.com",
    "key": "sale",
    "url": "https://shop.example.com/sale"
  },
  "matchMethod": "universal_link",
  "platform": "ios"
}
Trên Android, cùng request này trả về "matchMethod": "app_link""platform": "android".

Giá trị matchMethod

Giá trịÝ nghĩa
universal_linkiOS, app đã cài (immediate)
app_linkAndroid, app đã cài (immediate)
clipboardiOS deferred, khớp qua li2_cid trên clipboard
install_referrerAndroid deferred, khớp qua Google Play Install Referrer

Giá trị missReason

Giá trịÝ nghĩa
no_candidateTra cứu clipboard không có kết quả (hoặc bản ghi đã hết hạn)
clipboard_deniedNgười dùng chặn quyền dán trên iOS
clipboard_emptyClipboard rỗng (không có link Li2)
opt_outNgười dùng chủ động bỏ qua
cross_tenant_blockedHostname thuộc organization khác — bị chặn theo cô lập tenant

Mã trạng thái

StatusKhi nào
200Thành công (kể cả khi trượt — link: null + missReason)
400Body sai định dạng / clipboardStatus không hợp lệ
401Thiếu hoặc sai X-Li2-Key (hoặc gửi nhầm server API key)
403Org không có gói Pro; hoặc (luồng immediate) domain không thuộc org / deep link bị tắt
404Không tìm thấy slug (touch point)
429Vượt 1000 request/phút/IP. Server đặt header X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset; hãy backoff theo đó.
Cô lập tenant ở luồng deferred (clipboard/referrer) không trả 403 mà trả 200 + missReason: cross_tenant_blocked (link: null) — để không lộ việc hostname có thuộc org khác hay không. Chỉ luồng immediate mới trả 403 khi domain không thuộc org.

Hình dạng body lỗi

Response thành công là object phẳng ({ clickId, link, ... } như trên — không bọc envelope). Mọi response lỗi dùng chung một envelope:
Error
{
  "code": 403,
  "message": "Deep link attribution requires the Pro plan",
  "data": null,
  "error_code": "FEATURE_NOT_ALLOWED"
}
  • code = HTTP status; message = mô tả tiếng Anh dễ đọc; data luôn null khi lỗi.
  • error_codemã ổn định, máy đọc được — hãy branch theo nó thay vì so khớp chuỗi message. Hầu hết lỗi phía client (4xx) đều kèm error_code; lỗi server (5xx) trả cùng envelope nhưng khôngerror_code (chỉ cần retry/backoff). Ngoại lệ quan trọng: lỗi xác thực sinh ở tầng middleware (key thiếu/sai, origin sai) không kèm error_code — xem cảnh báo bên dưới.

Các error_code của endpoint

HTTPerror_codeKhi nào
400INVALID_JSONBody không parse được
400MISSING_REQUIRED_FIELDThiếu cả deepLink, li2Domains lẫn installReferrer
400INVALID_FIELD_FORMATclipboardStatus/li2Domains sai định dạng, hoặc deepLink không phải URL hợp lệ
400INVALID_CLICK_IDli2_cid không đúng 16 ký tự base62
401MISSING_API_KEYThiếu / không xác thực được X-Li2-Key
403FEATURE_NOT_ALLOWEDOrg không có gói Pro
403CROSS_ORG_ATTRIBUTION(luồng immediate) domain không thuộc org của key
404LINK_NOT_FOUNDKhông tìm thấy slug (touch point)
Lỗi xác thực ở tầng middleware không có error_code. Bảng trên là các mã do controller phát ra sau khi đã qua xác thực. Nhưng khi X-Li2-Key thiếu hoặc sai, hoặc origin không hợp lệ (luồng web SDK), request bị chặn ngay ở middleware (AuthMobileApikeyMiddleware) — và những phản hồi đó dùng envelope cũ không có trường error_code:
{ "code": 401, "message": "Missing X-Li2-Key header", "data": null }
Hệ quả thực tế cho client: với lỗi xác thực (401/403 từ middleware) hãy branch theo HTTP status; chỉ với lỗi validation/nghiệp vụ (4xx từ controller) mới có error_code để branch. Cụ thể: MISSING_API_KEY (401) chỉ xuất hiện ở một nhánh nội bộ của controller, không phải mã bạn nhận khi gửi key sai — trường hợp đó là 401 không kèm error_code. Tương tự, CROSS_ORG_ATTRIBUTION là mã của controller (luồng immediate, domain không thuộc org); còn 403 do origin sai ở middleware thì không kèm error_code.

Bước tiếp theo

Tích Hợp iOS & Android

Xem cách gọi endpoint này từ app native.

Tổng Quan Deep Link

Quay lại bức tranh tổng thể về immediate vs deferred.