Seguiment d'obertures i de clics
GET /tracking: si un missatge s'ha llegit i què s'ha seguit.
Executa qualsevol de les 6 crides d'aquesta pàgina contra el teu espai de treball, amb la teva pròpia clau.
Què es registra
Dos interruptors independents, tots dos activats tret que s'hagin desactivat per a l'adreça des de la qual s'envia un missatge o per a Totes les adreces. opens hi afegeix una imatge d'1×1; clicks reescriu els enllaços de la part nova del cos. L'historial citat sota una resposta és el missatge d'una altra persona i no es toca. Un enviament indica tracking: { opens, clicks } per decidir-ho per a un sol missatge (en qualsevol dels dos sentits, de manera que false és com un programa refusa el que l'adreça té configurat), i un camp que ometeu recau en la configuració de l'adreça des de la qual s'envia, i després en la de Totes les adreces, i no pas en un valor per defecte que aquesta API hagi triat en nom d'un espai de treball.
{ "from": "Acme Billing <[email protected]>", "to": ["[email protected]"], "subject": "Your September invoice", "html": "<p>Invoice attached.</p>", "tracking": { "opens": true, "clicks": true } }Es reescriuen com a màxim 100 destinacions per missatge, una vegada cadascuna. El mateix URL enllaçat des d'una imatge de capçalera, un botó i un peu és una sola fila, perquè és una sola pregunta feta tres vegades. Passat el límit, la resta d'enllaços es deixen exactament tal com es van escriure: un enllaç sense seguiment continua funcionant, i un missatge que perd en silenci els seus darrers dos-cents enllaços és una fallada molt pitjor que un informe incomplet.
Els enllaços reescrits i el píxel apunten per defecte a l'amfitrió de l'API d'OpenEmail. Quan el domini emissor té un domini de seguiment propi amb tracking.status active, el correu nou d'aquest domini utilitza https://<tracking host>/t/... en lloc d'això, i PATCH /domains/{id} és on se'n configura un.
Tot això requereix emails:read, i no hi ha cap àmbit de seguiment. Aquest àmbit ja vol dir «llegir els missatges enviats i el seu estat de lliurament», i si algú ha obert un missatge és l'estat de lliurament més literal possible.
Els endpoints
| Crida | Retorna |
|---|---|
| `GET /tracking` | Els missatges amb seguiment, els més nous primer. opened, clicked, days (1–365, per defecte 30), limit (màx. 200). |
| `GET /tracking/stats` | Taxes en una finestra. days (per defecte 30) i offsetMinutes, perquè els dies es tallin allà on es talla el dia de qui llegeix. |
| `GET /tracking/{id}` | Un sol informe. Accepta un id de seguiment tmsg_ o l'id msg_ que ha retornat un enviament. |
| `GET /tracking/{id}/opens` | Les peticions individuals. includeMachine, limit (màx. 200). |
| `GET /tracking/{id}/clicks` | El mateix, amb linkId i url a cada fila. |
| `GET /emails/{id}/tracking` | El mateix informe, a partir de l'id d'enviament que ja teniu. |
Els booleans s'escriuen explícitament a la cadena de consulta: true, false, 1 o 0, i qualsevol altra cosa es rebutja. Boolean("false") és cert, de manera que un ?opened=false convertit per coerció retornaria exactament el contrari del que s'ha demanat.
Això és un recurs propi i no uns quants camps a /emails per una qüestió de cobertura: aquella llista conté registres d'enviament, i el redactor, les eines MCP i l'assistent envien sense escriure'n cap. Un informe construït a partir d'aquella llista seria un informe sobre el vostre trànsit d'API i no sobre la bústia.
L'informe
{ "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 són el que s'ha APLICAT al missatge; opened i clicked són el que ha passat. openCount compta lectures i openCountRaw compta peticions. La diferència, aquí de quatre, són els escàners i els proxies de privadesa, que es conserven perquè la distància entre el registre i el total es pugui inspeccionar en lloc de quedar sense explicació. attributable és el camp que cal llegir abans d'anomenar ningú: false vol dir que una lectura ha caigut en una còpia que es va enviar a tota la llista, i tota frase sobre un destinatari concret a partir d'aquí és una suposició.
source anomena la superfície que l'ha enviat: api per a un enviament a través d'aquesta API, composer per a tot el que ha enviat la mateixa aplicació. sendId és null per al segon cas, i per això existeix l'id de seguiment.
Una fila amb email null i attributed: false és on cau una lectura que no s'ha pogut atribuir a cap persona, i un informe només en mostra una quan una lectura hi ha caigut de debò. Un missatge amb un sol destinatari no en té cap, perquè un cos i un destinatari són la mateixa afirmació. Un missatge amb diversos en té una al darrere des del moment en què surt, perquè el transport no queda fixat fins a l'expedició, i es manté fora de l'informe fins que hi arriba alguna cosa: un «algú: no obert» permanent al costat dels destinataris anomenats és una fila que només es pot malinterpretar. Quan SÍ que hi és, les files amb nom són les que estan a zero i attributable és false. La lectura és real, qui llegeix és una de les persones del missatge, i «algú d'aquest missatge» és l'única representació que les dades sostenen. No ompliu mai el nom a partir de la llista de destinataris.
Taxes en una finestra
{ "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 }] }Les taxes són percentatges sobre els missatges AMB SEGUIMENT, no sobre tot el correu enviat: un espai de treball que fa seguiment d'un missatge de cada deu té una taxa d'obertura per a aquells deu, i dividir per tot el que ha enviat mai baixaria cada vegada que algú enviés una resposta sense seguiment. Un missatge obert cinc vegades és UN missatge obert. Les taxes compten missatges i els totals compten impactes, i confondre els dos és com es publiquen taxes d'obertura superiors al 100%.
byDay és dispers: un dia en què no s'ha fet cap seguiment hi falta en lloc de sortir a zero, així que ompliu els buits abans de representar-ho en un gràfic. Els dies s'agrupen a offsetMinutes a l'est d'UTC (−840 a 840) perquè es tallin allà on es talla el dia de qui llegeix. medianTimeToOpenSeconds és una mediana i no una mitjana, perquè un sol missatge obert tres setmanes tard arrossega una mitjana cap a un lloc on no hi ha cap missatge.
Els impactes individuals
{ "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 és human, proxy o machine, i counted diu si ha mogut les xifres. Els impactes de màquina s'exclouen si no passeu includeMachine=true, que és el valor per defecte honest: es registren perquè descartar-los deixaria un buit inexplicable, no perquè siguin interacció real.
La ubicació és aproximada perquè no n'hi ha cap més. No es desa cap adreça IP de cap impacte. El país, la regió i la ciutat són el que la vora de la xarxa ja sabia, i l'únic altre identificador que es conserva és un hash amb una sal que rota cada dia, de manera que pot distingir dues peticions dins d'un mateix dia i queda inert l'endemà.
Què no poden dir les xifres
- Apple Mail Privacy Protection descarrega totes les imatges de tots els missatges en el moment del lliurament, tant si algú els mira com si no. Es classifica a partir del User-Agent i de la xarxa i es registra com a
machine, i també ho fa qualsevol cosa que arribi dins dels deu segons posteriors a l'enviament, perquè res del que fa una persona passa tan de pressa. - El proxy d'imatges de Gmail és
proxyi nomachine: algú ha visualitzat el missatge, així que l'obertura és real, mentre que el dispositiu, el client i la ubicació no es poden saber. El proxy també fa memòria cau, de manera que una segona lectura pot no arribar-nos mai. Els recomptes a través de Gmail són un mínim, mai un total. - Dues peticions de la mateixa còpia dins de trenta segons són una sola lectura. Una subfinestra de previsualització que es torna a dibuixar o un missatge que torna a la vista en desplaçar-s'hi tornen a demanar la imatge; la segona visita genuïna al cap d'una hora sí que es compta.
- Per anomenar el destinatari cal un missatge prou petit per reconstruir-lo persona a persona: la mida estimada multiplicada pel nombre de destinataris ha de quedar per sota de 8MB. Per sobre d'això, un sol cos va a tothom i tots els impactes que hi arriben queden sense atribuir.
- Un missatge amb clics i sense obertures s'ha llegit amb tota seguretat: les imatges es bloquegen molt més sovint del que els enllaços es queden sense clicar. Llegiu els dos comptadors per separat en lloc de sumar-los.
- Demanar el seguiment de clics en un cos sense enllaços no registra absolutament res: els bytes que surten són idèntics als d'un enviament sense seguiment, i una fila que digués el contrari no es podria conciliar amb res. El mateix passa amb un missatge sense cos per reescriure.
- OpenEmail elimina les imatges d'1×1 del correu que llegeixen els seus propis usuaris, inclòs el píxel que envia ell mateix, i registra l'obertura quan un missatge es mostra amb les imatges visibles. Aquest impacte és
humanamb el clientOpenEmail. Amb les imatges amagades no es registra res.
GET /tracking/{id} i GET /emails/{id}/tracking responen 404 per a un missatge que mai no ha tingut seguiment, en lloc d'un informe buit. Les frases «no hem registrat res» i «ningú no l'ha obert» són respostes diferents i no poden compartir una mateixa resposta. L'endpoint de llista només conté missatges amb seguiment, de manera que un missatge sense seguiment simplement no hi surt, en comptes de sortir-hi amb zeros.
Que t'ho diguin en lloc de preguntar-ho
Una obertura comptada dispara email.opened i un clic comptat dispara email.clicked a tots els endpoints subscrits, i tots dos s'escriuen al rastre d'esdeveniments del mateix missatge quan ha passat per aquesta API. Cap dels dos no es dispara per a un escàner o un proxy de privadesa. Enviar-los ompliria el registre de qui els rep exactament amb el trànsit que el classificador existeix per mantenir fora de les xifres.
Un fitxer que ha sortit com a enllaç de descàrrega s'informa de la mateixa manera. Una descàrrega comptada dispara email.downloaded i cau al mateix rastre, i el mateix classificador en manté fora els escàners i els previsualitzadors d'enllaços, de manera que el recompte són persones. La càrrega útil anomena el fitxer (shareId, fileId, filename, mimeType, sizeBytes, url) amb downloadCount, first i downloadedAt al costat dels camps de client i ubicació que porta un clic. recipient és sempre null i attributed sempre false: un enllaç de descàrrega és un sol URL per a tots els destinataris del missatge, així que una descàrrega no es pot atribuir a cap d'ells.
Des de l'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(),})Totes les crides d'aquí són lectures simples, i el client reintenta cadascuna pel seu compte. get llança un OpenEmailApiError amb isNotFound cert per a un missatge que mai no ha tingut seguiment, que és la distinció que val la pena conservar allà on hi aboqueu el resultat.