Theo dõi lượt mở và lượt nhấp
GET /tracking: thư đã được đọc chưa, và liên kết nào đã được nhấp.
Chạy bất kỳ lệnh nào trong 6 lệnh gọi trên trang này với không gian làm việc của bạn, bằng khóa của chính bạn.
Những gì được ghi lại
Hai công tắc độc lập, cả hai đều bật trừ khi đã bị tắt cho địa chỉ gửi thư hoặc cho Tất cả địa chỉ. opens chèn thêm một ảnh 1×1; clicks viết lại các liên kết trong phần mới của nội dung. Lịch sử được trích dẫn bên dưới một thư trả lời là thư của người khác và được giữ nguyên. Một lần gửi chỉ định tracking: { opens, clicks } để quyết định cho riêng thư đó (theo cả hai chiều, nên false là cách một chương trình từ chối hành vi mà địa chỉ được thiết lập), và trường bạn bỏ qua sẽ dùng cài đặt của địa chỉ gửi, rồi đến Tất cả địa chỉ, chứ không dùng một giá trị mặc định mà API này tự chọn thay cho không gian làm việc.
{ "from": "Acme Billing <[email protected]>", "to": ["[email protected]"], "subject": "Your September invoice", "html": "<p>Invoice attached.</p>", "tracking": { "opens": true, "clicks": true } }Mỗi thư được viết lại tối đa 100 đích đến, mỗi đích một lần. Cùng một URL được liên kết từ ảnh đầu thư, một nút bấm và phần chân thư là một hàng, vì đó là một câu hỏi được hỏi ba lần. Vượt quá giới hạn, các liên kết còn lại được giữ nguyên như đã viết: một liên kết không được theo dõi vẫn hoạt động, và một thư âm thầm mất hai trăm liên kết cuối là lỗi tệ hơn nhiều so với một báo cáo chưa đầy đủ.
Liên kết được viết lại và pixel mặc định trỏ tới host API của OpenEmail. Khi tên miền gửi có tên miền theo dõi tùy chỉnh với tracking.status là active, thư mới từ tên miền đó sẽ dùng https://<tracking host>/t/... thay thế, và PATCH /domains/{id} là nơi bạn thiết lập nó.
Tất cả những điều này cần emails:read, và không có phạm vi riêng cho việc theo dõi. Phạm vi đó vốn đã có nghĩa là “đọc thư đã gửi và trạng thái chuyển phát của chúng”, và việc ai đó đã mở thư hay chưa là trạng thái chuyển phát theo nghĩa đen nhất.
Các endpoint
| Lệnh gọi | Trả về |
|---|---|
| `GET /tracking` | Các thư được theo dõi, mới nhất trước. opened, clicked, days (1–365, mặc định 30), limit (tối đa 200). |
| `GET /tracking/stats` | Tỷ lệ trong một khoảng thời gian. days (mặc định 30) và offsetMinutes, để ranh giới ngày trùng với ranh giới ngày của người đọc. |
| `GET /tracking/{id}` | Một báo cáo. Nhận id theo dõi tmsg_ hoặc id msg_ mà một lần gửi trả về. |
| `GET /tracking/{id}/opens` | Từng lượt tải riêng lẻ. includeMachine, limit (tối đa 200). |
| `GET /tracking/{id}/clicks` | Tương tự, với linkId và url trên mỗi hàng. |
| `GET /emails/{id}/tracking` | Cùng báo cáo đó, lấy từ id lần gửi mà bạn đã có. |
Giá trị boolean phải được viết rõ trong query string: true, false, 1 hoặc 0, mọi giá trị khác đều bị từ chối. Boolean("false") là true, nên một ?opened=false bị ép kiểu sẽ trả về kết quả ngược hẳn với yêu cầu.
Đây là một tài nguyên riêng thay vì vài trường trên /emails vì lý do phạm vi bao phủ: danh sách đó chứa các bản ghi gửi, trong khi trình soạn thư, các công cụ MCP và trợ lý đều gửi thư mà không tạo bản ghi nào. Một báo cáo dựng trên đó sẽ là báo cáo về lưu lượng API của bạn chứ không phải về hộp thư.
Báo cáo
{ "object": "tracking", "id": "tmsg_9c1f7b2e4a5d40b8a3e61d2f", "sendId": "msg_c5f21cc6bfec4e848caf905b", "threadId": "thread_2f9b…", "messageId": "<2598…@acme.com>", "subject": "Your September invoice", "from": "[email protected]", "source": "api", "sentAt": "2026-08-29T08:19:08.000Z", "opens": true, "clicks": true, "opened": true, "clicked": true, "attributable": true, "openCount": 3, "openCountRaw": 7, "clickCount": 1, "clickCountRaw": 2, "firstOpenAt": "2026-08-29T09:04:11.000Z", "lastOpenAt": "2026-08-30T07:42:55.000Z", "firstClickAt": "2026-08-29T09:05:02.000Z", "lastClickAt": "2026-08-29T09:05:02.000Z", "recipients": [ { "email": "[email protected]", "kind": "to", "attributed": true, "openCount": 3, "clickCount": 1, "firstOpenAt": "2026-08-29T09:04:11.000Z", "lastOpenAt": "2026-08-30T07:42:55.000Z", "firstClickAt": "2026-08-29T09:05:02.000Z", "lastClickAt": "2026-08-29T09:05:02.000Z" } ], "links": [ { "id": "lnk_4f0a1c8d29b74e6fa3c05d17", "url": "https://acme.com/invoices/42", "label": "View invoice", "clickCount": 1, "clickCountRaw": 2 } ] }opens và clicks là những gì đã được ÁP DỤNG cho thư; opened và clicked là những gì đã xảy ra. openCount đếm lượt đọc và openCountRaw đếm lượt tải. Phần chênh lệch, ở đây là bốn, là từ các trình quét và proxy bảo vệ quyền riêng tư, được giữ lại để khoảng cách giữa nhật ký và tổng số có thể kiểm tra được thay vì không giải thích được. attributable là trường cần đọc trước khi nêu tên bất kỳ ai: false nghĩa là một lượt đọc rơi vào bản sao được gửi cho cả danh sách, và mọi câu nói về một người nhận cụ thể sau đó đều chỉ là phỏng đoán.
source cho biết nơi đã gửi thư: api cho lần gửi qua API này, composer cho mọi thư do chính ứng dụng gửi. sendId là null với loại thứ hai, đó là lý do id theo dõi tồn tại.
Một hàng có email là null và attributed: false là nơi ghi nhận một lượt đọc không thể gắn với một người cụ thể, và báo cáo chỉ hiển thị hàng này khi thực sự có một lượt đọc như vậy. Thư chỉ có một người nhận hoàn toàn không có hàng này, vì một nội dung và một người nhận là cùng một điều. Thư có nhiều người nhận có sẵn hàng này ngay từ lúc được gửi đi, vì tầng vận chuyển chỉ được chốt khi gửi đi, và hàng này không xuất hiện trong báo cáo cho tới khi có gì đó được ghi vào: một dòng cố định “ai đó: chưa mở” bên cạnh những người nhận có tên là một hàng chỉ có thể bị hiểu sai. Khi hàng này CÓ mặt, các hàng có tên là những hàng đang ở mức không và attributable là false. Lượt đọc là thật, người đọc là một trong những người có trong thư, và “ai đó trong thư này” là cách hiển thị duy nhất mà dữ liệu cho phép. Đừng bao giờ điền tên từ danh sách người nhận.
Tỷ lệ trong một khoảng thời gian
{ "object": "tracking_stats", "tracked": 128, "trackedForOpens": 128, "trackedForClicks": 47, "opened": 91, "clicked": 34, "openRate": 71.1, "clickRate": 72.3, "totalOpens": 240, "totalClicks": 52, "machineOpens": 173, "medianTimeToOpenSeconds": 2714, "byDay": [{ "day": "2026-08-27", "sent": 12, "opened": 9, "clicked": 3 }], "topLinks": [{ "url": "https://acme.com/pricing", "label": "See pricing", "clickCount": 18 }], "clients": [{ "client": "Gmail", "count": 96 }], "countries": [{ "country": "GB", "count": 71 }] }Các tỷ lệ là phần trăm trên số thư ĐƯỢC THEO DÕI, không phải trên toàn bộ thư đã gửi: một không gian làm việc theo dõi một trong mười thư sẽ có tỷ lệ mở cho mười thư đó, và chia cho tất cả thư từng gửi sẽ làm tỷ lệ giảm mỗi khi ai đó gửi một thư trả lời không được theo dõi. Một thư được mở năm lần là MỘT thư đã mở. Tỷ lệ đếm số thư còn tổng số đếm số lượt, và nhầm lẫn giữa hai thứ này là cách những tỷ lệ mở trên 100% được công bố.
byDay là dữ liệu thưa: ngày không có gì được theo dõi sẽ vắng mặt chứ không phải bằng không, vì vậy hãy điền các khoảng trống trước khi vẽ biểu đồ. Các ngày được nhóm theo offsetMinutes về phía đông của UTC (−840 đến 840) để ranh giới ngày trùng với ranh giới ngày của người đọc. medianTimeToOpenSeconds là trung vị chứ không phải trung bình, vì một thư được mở muộn ba tuần sẽ kéo giá trị trung bình tới một chỗ mà không thư nào thực sự ở đó.
Từng lượt truy cập
{ "object": "list", "data": [ { "object": "open", "id": "opn_1a7c…", "trackedMessageId": "tmsg_9c1f7b2e4a5d40b8a3e61d2f", "recipient": "[email protected]", "kind": "machine", "counted": false, "client": "Apple Mail Privacy Protection", "device": "unknown", "os": "macOS", "country": "GB", "region": "England", "city": "London", "createdAt": "2026-08-29T08:19:11.000Z" } ] }kind là human, proxy hoặc machine, và counted cho biết lượt đó có được tính vào số liệu hay không. Lượt truy cập từ máy bị loại trừ trừ khi bạn truyền includeMachine=true, đây là mặc định trung thực: chúng được ghi lại vì bỏ đi sẽ để lại một khoảng trống không giải thích được, chứ không phải vì chúng là tương tác.
Vị trí chỉ ở mức ước lượng vì đó là tất cả những gì có được. Không có địa chỉ IP nào được lưu cho bất kỳ lượt truy cập nào. Quốc gia, vùng và thành phố là những gì máy chủ biên đã biết sẵn, và định danh duy nhất khác được giữ là một giá trị băm có salt được xoay vòng hằng ngày, nên nó phân biệt được hai lượt tải trong cùng một ngày và vô hiệu vào ngày hôm sau.
Những điều số liệu không thể cho biết
- Apple Mail Privacy Protection tải mọi ảnh trong mọi thư khi thư được chuyển tới, dù có ai xem hay không. Lượt này được phân loại dựa trên User-Agent và mạng, rồi ghi nhận là
machine, và mọi lượt đến trong vòng mười giây sau khi gửi cũng vậy, vì không có hành động nào của con người diễn ra nhanh như thế. - Proxy hình ảnh của Gmail là
proxychứ không phảimachine: có người đã hiển thị thư, nên lượt mở là thật, nhưng thiết bị, ứng dụng thư và vị trí thì không thể biết được. Proxy này cũng lưu bộ nhớ đệm, nên lượt đọc thứ hai có thể không bao giờ tới được chúng tôi. Số liệu qua Gmail là mức tối thiểu, không bao giờ là tổng số. - Hai lượt tải cùng một bản sao trong vòng ba mươi giây được tính là một lượt đọc. Khung xem trước vẽ lại hoặc một thư được cuộn trở lại vào tầm nhìn sẽ tải lại ảnh; lượt xem thứ hai thực sự sau đó một giờ vẫn được tính.
- Để nêu tên người nhận, thư phải đủ nhỏ để dựng lại cho từng người: kích thước ước tính nhân với số người nhận phải dưới 8MB. Vượt quá mức đó, một nội dung duy nhất được gửi cho tất cả mọi người, và mọi lượt truy cập trên nó đều không gắn với ai.
- Một thư có lượt nhấp mà không có lượt mở chắc chắn đã được đọc: hình ảnh bị chặn thường xuyên hơn nhiều so với việc liên kết không được nhấp. Hãy đọc riêng hai bộ đếm thay vì cộng chúng lại.
- Yêu cầu theo dõi lượt nhấp trên một nội dung không có liên kết sẽ không ghi lại gì cả: dữ liệu byte được gửi đi giống hệt một lần gửi không theo dõi, và một hàng khẳng định điều ngược lại sẽ không đối chiếu được với bất cứ thứ gì. Điều tương tự cũng đúng với thư không có nội dung để viết lại.
- OpenEmail loại bỏ các ảnh 1×1 khỏi thư mà chính người dùng của nó đọc, bao gồm cả pixel mà nó gửi, và tự ghi nhận lượt mở khi thư được hiển thị với hình ảnh đang bật. Lượt đó là
humanvới ứng dụng thưOpenEmail. Khi hình ảnh bị ẩn, không có gì được ghi lại.
GET /tracking/{id} và GET /emails/{id}/tracking trả về 404 cho thư chưa từng được theo dõi, thay vì một báo cáo rỗng. “Chúng tôi không ghi nhận gì” và “không ai mở thư” là hai câu trả lời khác nhau và không được dùng chung một phản hồi. Endpoint danh sách chỉ chứa các thư được theo dõi, nên thư không được theo dõi đơn giản là không có trong đó thay vì có mặt với các giá trị bằng không.
Được thông báo thay vì phải hỏi
Một lượt mở được tính sẽ kích hoạt email.opened và một lượt nhấp được tính sẽ kích hoạt email.clicked tới mọi endpoint đã đăng ký, và cả hai đều được ghi vào lịch sử sự kiện của chính thư đó nếu thư được gửi qua API này. Không sự kiện nào được kích hoạt cho trình quét hay proxy bảo vệ quyền riêng tư. Đẩy những lượt đó đi sẽ làm đầy nhật ký của bên nhận bằng chính lưu lượng mà bộ phân loại được tạo ra để loại khỏi số liệu.
Một tệp được gửi đi dưới dạng liên kết tải xuống cũng được báo cáo theo cùng cách. Một lượt tải xuống được tính sẽ kích hoạt email.downloaded và được ghi vào cùng lịch sử sự kiện, và cùng bộ phân loại đó loại trừ trình quét và trình xem trước liên kết, nên số lượt là của con người. Payload nêu thông tin tệp (shareId, fileId, filename, mimeType, sizeBytes, url) cùng downloadCount, first và downloadedAt bên cạnh các trường ứng dụng thư và vị trí mà một lượt nhấp mang theo. recipient luôn là null và attributed luôn là false: liên kết tải xuống là một URL duy nhất cho mọi người nhận của thư, nên không thể gắn một lượt tải xuống với một người cụ thể trong số họ.
Từ SDK
const report = await openemail.tracking.get('msg_c5f21cc6bfec…')const cold = await openemail.tracking.list({ days: 30, opened: false })const stats = await openemail.tracking.getStats({ days: 30, offsetMinutes: -new Date().getTimezoneOffset(),})Mọi lệnh gọi ở đây đều là thao tác đọc đơn thuần, và client tự thử lại từng lệnh. get ném ra OpenEmailApiError có isNotFound là true với thư chưa từng được theo dõi, đây là sự phân biệt đáng giữ lại trong bất cứ thứ gì bạn đưa dữ liệu vào.