Śledzenie otwarć i kliknięć
GET /tracking: czy wiadomość została przeczytana i w które odnośniki kliknięto.
Uruchamia dowolne z 6 wywołań na tej stronie na twojej przestrzeni roboczej, twoim własnym kluczem.
Co jest zapisywane
Dwa niezależne przełączniki, oba włączone, chyba że wyłączono je dla adresu, z którego wiadomość jest wysyłana, albo dla ustawienia „Wszystkie adresy”. opens dokleja obrazek 1×1; clicks przepisuje odnośniki w nowej części treści. Cytowana historia pod odpowiedzią jest czyjąś cudzą wiadomością i zostaje nietknięta. Wysyłka podaje tracking: { opens, clicks }, żeby zdecydować o jednej wiadomości (w obie strony, więc false to sposób, w jaki program odmawia tego, co adres ma ustawione), a pole, które pominiesz, przyjmuje ustawienie adresu, z którego wiadomość wychodzi, potem ustawienie „Wszystkie adresy” — a nie wartość domyślną, którą to API wybrało za przestrzeń roboczą.
{ "from": "Acme Billing <[email protected]>", "to": ["[email protected]"], "subject": "Your September invoice", "html": "<p>Invoice attached.</p>", "tracking": { "opens": true, "clicks": true } }Przepisywanych jest najwyżej 100 adresów docelowych na wiadomość, każdy raz. Ten sam URL podlinkowany z obrazka nagłówka, z przycisku i ze stopki to jeden wiersz, bo to jedno pytanie zadane trzy razy. Powyżej limitu pozostałe odnośniki zostają dokładnie takie, jakie zostały napisane: nieśledzony odnośnik nadal działa, a wiadomość, która po cichu gubi ostatnie dwieście odnośników, to awaria znacznie gorsza niż niekompletny raport.
Przepisane odnośniki i piksel domyślnie wskazują na host API OpenEmail. Gdy domena nadawcza ma własną domenę śledzącą, której tracking.status to active, nowa poczta z tej domeny używa zamiast tego https://<tracking host>/t/..., a ustawia się ją przez PATCH /domains/{id}.
Wszystko to wymaga emails:read i nie ma osobnego zakresu dla śledzenia. Ten zakres już teraz oznacza „czytaj wysłane wiadomości i ich status dostarczenia”, a to, czy ktoś otworzył wiadomość, jest najbardziej dosłownym możliwym statusem dostarczenia.
Endpointy
| Wywołanie | Zwraca |
|---|---|
| `GET /tracking` | Śledzone wiadomości, od najnowszych. opened, clicked, days (1–365, domyślnie 30), limit (maks. 200). |
| `GET /tracking/stats` | Wskaźniki w oknie czasu. days (domyślnie 30) i offsetMinutes, żeby doby łamały się tam, gdzie łamie się doba czytającego. |
| `GET /tracking/{id}` | Jeden raport. Przyjmuje identyfikator śledzenia tmsg_ albo identyfikator msg_ zwrócony przez wysyłkę. |
| `GET /tracking/{id}/opens` | Poszczególne pobrania. includeMachine, limit (maks. 200). |
| `GET /tracking/{id}/clicks` | To samo, z linkId i url w każdym wierszu. |
| `GET /emails/{id}/tracking` | Ten sam raport, na podstawie identyfikatora wysyłki, który już masz. |
Wartości logiczne zapisuje się w query stringu wprost: true, false, 1 albo 0, a cokolwiek innego jest odrzucane. Boolean("false") daje true, więc rzutowane ?opened=false zwróciłoby dokładną odwrotność tego, o co poproszono.
To osobny zasób, a nie kilka pól na /emails, z powodu pokrycia: tamta lista zawiera rekordy wysyłek, a kompozytor, narzędzia MCP i asystent wysyłają, nie zapisując takiego rekordu. Raport zbudowany na niej byłby raportem o twoim ruchu w API, a nie o skrzynce.
Raport
{ "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 i clicks mówią, co ZASTOSOWANO do wiadomości; opened i clicked mówią, co się wydarzyło. openCount liczy odczyty, a openCountRaw liczy pobrania. Różnica, tutaj cztery, to skanery i proxy prywatności, zachowane po to, by lukę między dziennikiem a sumą dało się zbadać, zamiast zostawiać ją bez wyjaśnienia. attributable to pole, które trzeba przeczytać, zanim wskaże się kogokolwiek: false oznacza, że odczyt trafił na kopię, która poszła do całej listy, i każde zdanie o konkretnym odbiorcy jest od tego momentu zgadywaniem.
source nazywa powierzchnię, która wysłała wiadomość: api dla wysyłki przez to API, composer dla wszystkiego, co wysłała sama aplikacja. sendId jest null dla tego drugiego rodzaju i właśnie dlatego istnieje identyfikator śledzenia.
Wiersz z pustym email i attributed: false to miejsce, w którym ląduje odczyt niedający się przypisać do osoby, a raport pokazuje taki wiersz tylko wtedy, gdy odczyt faktycznie nastąpił. Wiadomość z jednym odbiorcą nie ma go wcale, bo jedna treść i jeden adresat to to samo stwierdzenie. Wiadomość z kilkoma odbiorcami ma go za sobą od chwili wyjścia, ponieważ transport jest ustalony dopiero przy wysyłce, a wiersz pozostaje poza raportem, dopóki coś na niego nie trafi: trwałe „ktoś: nie otworzył” obok nazwanych odbiorców to wiersz, który można tylko źle odczytać. Tam, gdzie JEST obecny, nazwane wiersze są tymi, które stoją na zerze, a attributable ma wartość false. Odczyt jest prawdziwy, czytający to jedna z osób na wiadomości, a „ktoś z tej wiadomości” jest jedynym renderowaniem, które dane uzasadniają. Nigdy nie uzupełniaj nazwiska z listy odbiorców.
Wskaźniki w oknie czasu
{ "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 }] }Wskaźniki są procentami liczonymi po wiadomościach ŚLEDZONYCH, a nie po całej wysłanej poczcie: przestrzeń robocza, która śledzi jedną wiadomość na dziesięć, ma wskaźnik otwarć dla tych dziesięciu, a dzielenie przez wszystko, co kiedykolwiek wysłała, obniżałoby go za każdym razem, gdy ktoś wyśle nieśledzoną odpowiedź. Wiadomość otwarta pięć razy to JEDNA otwarta wiadomość. Wskaźniki liczą wiadomości, a sumy liczą trafienia, i mylenie jednego z drugim to sposób, w jaki publikuje się wskaźniki otwarć powyżej 100%.
byDay jest rzadkie: dzień, w którym nic nie było śledzone, jest nieobecny, a nie zerowy, więc przed wykresem uzupełnij luki. Doby są kubełkowane offsetMinutes na wschód od UTC (−840 do 840), żeby łamały się tam, gdzie łamie się doba czytającego. medianTimeToOpenSeconds jest medianą, a nie średnią, ponieważ jedna wiadomość otwarta trzy tygodnie później ciągnie średnią tam, gdzie nie ma żadnej wiadomości.
Poszczególne trafienia
{ "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 to human, proxy albo machine, a counted mówi, czy trafienie poruszyło liczby. Trafienia maszynowe są pomijane, chyba że przekażesz includeMachine=true, co jest uczciwym domyślnym zachowaniem: są zapisywane dlatego, że ich pominięcie zostawiłoby niewyjaśnialną lukę, a nie dlatego, że są zaangażowaniem.
Lokalizacja jest zgrubna, bo to wszystko, co jest. Dla żadnego trafienia nie jest przechowywany adres IP. Kraj, region i miasto to to, co i tak wiedział edge, a jedynym innym zachowywanym identyfikatorem jest skrót, którego sól zmienia się codziennie, więc potrafi odróżnić dwa pobrania w obrębie doby i jest bezużyteczny nazajutrz.
Czego liczby nie powiedzą
- Apple Mail Privacy Protection pobiera każdy obrazek w każdej wiadomości przy dostarczeniu, niezależnie od tego, czy ktoś ją ogląda. Jest to klasyfikowane na podstawie User-Agent i sieci oraz zapisywane jako
machine; tak samo jest z wszystkim, co przychodzi w ciągu dziesięciu sekund od wysyłki, bo nic, co robi człowiek, nie dzieje się tak szybko. - Proxy obrazków Gmaila to
proxy, a niemachine: ktoś wyświetlił wiadomość, więc otwarcie jest prawdziwe, natomiast urządzenie, klient i lokalizacja są niepoznawalne. Proxy także cache'uje, więc drugi odczyt może w ogóle do nas nie dotrzeć. Liczby przechodzące przez Gmaila są dolną granicą, nigdy sumą. - Dwa pobrania tej samej kopii w ciągu trzydziestu sekund to jeden odczyt. Panel podglądu przerysowujący się albo wiadomość przewinięta z powrotem w pole widzenia ponownie pobierają obrazek; prawdziwa druga wizyta godzinę później nadal jest liczona.
- Wskazanie odbiorcy wymaga wiadomości na tyle małej, by dało się ją odbudować dla każdej osoby: szacowany rozmiar razy liczba odbiorców musi zmieścić się poniżej 8MB. Powyżej tego jedna treść idzie do wszystkich, a każde trafienie na niej jest nieprzypisane.
- Wiadomość z kliknięciami i bez otwarć na pewno została przeczytana: obrazki są blokowane o wiele częściej, niż odnośniki pozostają nieklikane. Czytaj oba liczniki osobno, zamiast je sumować.
- Prośba o śledzenie kliknięć w treści bez odnośników nie zapisuje niczego: bajty, które wyszły, są identyczne jak przy nieśledzonej wysyłce, a wiersz twierdzący inaczej nie dałby się z niczym pogodzić. To samo dotyczy wiadomości bez treści do przepisania.
- OpenEmail usuwa obrazki 1×1 z poczty, którą czytają jego właśni użytkownicy — włącznie z pikselem, który sam wysyła — i zapisuje otwarcie samodzielnie, gdy wiadomość jest wyświetlana z widocznymi obrazkami. Takie trafienie ma
humani klientaOpenEmail. Przy ukrytych obrazkach nic nie jest zapisywane.
GET /tracking/{id} i GET /emails/{id}/tracking odpowiadają 404 dla wiadomości, która nigdy nie była śledzona, zamiast pustym raportem. Sformułowania „nic nie zapisaliśmy” i „nikt tego nie otworzył” to różne odpowiedzi i nie mogą dzielić jednej odpowiedzi HTTP. Endpoint listy zawiera wyłącznie śledzone wiadomości, więc nieśledzona jest z niej po prostu nieobecna, a nie obecna z zerami.
Powiadomienie zamiast odpytywania
Policzone otwarcie wyzwala email.opened, a policzone kliknięcie email.clicked na każdym subskrybowanym endpoincie, i oba są zapisywane na własnym śladzie zdarzeń wiadomości tam, gdzie przeszła ona przez to API. Żadne z nich nie wyzwala się dla skanera ani proxy prywatności. Wypychanie ich zapełniłoby dziennik odbiorcy dokładnie tym ruchem, który klasyfikator ma trzymać z dala od liczb.
Plik, który wyszedł jako odnośnik do pobrania, raportuje się tak samo. Policzone pobranie wyzwala email.downloaded i ląduje na tym samym śladzie, a ten sam klasyfikator trzyma z dala skanery i podglądy odnośników, więc licznik to ludzie. Ładunek nazywa plik (shareId, fileId, filename, mimeType, sizeBytes, url) wraz z downloadCount, first i downloadedAt, obok pól klienta i lokalizacji, które niesie kliknięcie. recipient jest zawsze null, a attributed zawsze false: odnośnik do pobrania to jeden URL dla każdego odbiorcy wiadomości, więc pobrania nie da się przypisać do jednego z nich.
Z poziomu 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(),})Każde wywołanie tutaj jest zwykłym odczytem, a klient ponawia każde z osobna. get rzuca OpenEmailApiError, którego isNotFound jest true dla wiadomości, która nigdy nie była śledzona — i to jest rozróżnienie warte zachowania w tym, do czego to podajesz.