Openings- en kliktracking
GET /tracking: of een bericht is gelezen, en wat er is gevolgd.
Voert elk van de 6 aanroepen op deze pagina uit op je workspace, met je eigen sleutel.
Wat er wordt vastgelegd
Twee onafhankelijke schakelaars, beide aan tenzij ze zijn uitgezet voor het adres waarvandaan een bericht wordt verstuurd of voor Alle adressen. opens voegt een afbeelding van 1×1 toe; clicks herschrijft de links in het nieuwe deel van de body. De geciteerde geschiedenis onder een antwoord is het bericht van iemand anders en blijft ongemoeid. Een verzending noemt tracking: { opens, clicks } om het voor één bericht te bepalen (in beide richtingen, dus false is hoe een programma afwijst wat het adres zou doen), en een veld dat je weglaat valt terug op de instelling van het adres waarvandaan het wordt verstuurd, daarna op Alle adressen, en niet op een standaard die deze API namens een workspace heeft gekozen.
{ "from": "Acme Billing <[email protected]>", "to": ["[email protected]"], "subject": "Your September invoice", "html": "<p>Invoice attached.</p>", "tracking": { "opens": true, "clicks": true } }Er worden maximaal 100 bestemmingen per bericht herschreven, elk één keer. Dezelfde URL die vanuit een headerafbeelding, een knop en een footer wordt gelinkt is één rij, want het is één vraag die drie keer wordt gesteld. Voorbij de limiet blijven de overige links precies zoals ze geschreven zijn: een niet-getrackte link werkt nog steeds, en een bericht dat stilletjes zijn laatste tweehonderd links kwijtraakt is een veel ergere fout dan een onvolledig rapport.
Herschreven links en de pixel wijzen standaard naar de OpenEmail API-host. Wanneer het verzendende domein een eigen trackingdomein heeft waarvan tracking.status active is, gebruikt nieuwe post van dat domein in plaats daarvan https://<tracking host>/t/..., en PATCH /domains/{id} is waar je er een instelt.
Dit alles vereist emails:read; er is geen aparte tracking-scope. Die scope betekent al "verzonden berichten en hun bezorgstatus lezen", en of iemand een bericht heeft geopend is de meest letterlijke bezorgstatus die er is.
De endpoints
| Aanroep | Geeft terug |
|---|---|
| `GET /tracking` | Getrackte berichten, nieuwste eerst. opened, clicked, days (1–365, standaard 30), limit (max. 200). |
| `GET /tracking/stats` | Percentages over een venster. days (standaard 30) en offsetMinutes, zodat dagen breken waar de dag van de lezer breekt. |
| `GET /tracking/{id}` | Eén rapport. Accepteert een tmsg_-tracking-id of het msg_-id dat een verzending teruggaf. |
| `GET /tracking/{id}/opens` | De afzonderlijke ophaalacties. includeMachine, limit (max. 200). |
| `GET /tracking/{id}/clicks` | Hetzelfde, met linkId en url op elke rij. |
| `GET /emails/{id}/tracking` | Hetzelfde rapport, vanaf het verzend-id dat je al hebt. |
Booleans worden voluit geschreven in de query string: true, false, 1 of 0, en al het andere wordt geweigerd. Boolean("false") is true, dus een geforceerd omgezette ?opened=false zou precies het tegenovergestelde teruggeven van wat er gevraagd werd.
Dit is een eigen resource in plaats van een paar velden op /emails, vanwege dekking: die lijst bevat verzendrecords, en de composer, de MCP-tools en de assistent versturen allemaal zonder er een te schrijven. Een rapport dat daarop gebouwd is zou een rapport over je API-verkeer zijn in plaats van over de mailbox.
Het rapport
{ "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 en clicks zijn wat er op het bericht is TOEGEPAST; opened en clicked zijn wat er is gebeurd. openCount telt leesmomenten en openCountRaw telt ophaalacties. Het verschil, hier vier, zijn de scanners en de privacyproxy's, bewaard zodat het gat tussen het logboek en het totaal inspecteerbaar is in plaats van onverklaard. attributable is het veld om te lezen voordat je iemand bij naam noemt: false betekent dat een leesmoment landde op een kopie die naar de hele lijst ging, en elke zin over een specifieke ontvanger daarna is een gok.
source noemt het oppervlak dat het verstuurde: api voor een verzending via deze API, composer voor alles wat de app zelf verstuurde. sendId is null voor de tweede soort, en daarom bestaat het tracking-id.
Een rij met een null email en attributed: false is waar een leesmoment landt dat niet aan een persoon kon worden gekoppeld, en een rapport toont er alleen een wanneer er daadwerkelijk gelezen is. Een bericht met één ontvanger heeft er helemaal geen, want één body en één geadresseerde zijn dezelfde uitspraak. Een bericht met meerdere heeft er vanaf het moment van verzending een achter zich, want het transport ligt pas bij verzending vast, en die blijft buiten het rapport totdat er iets op binnenkomt: een permanente "iemand: niet geopend" naast de genoemde ontvangers is een rij die alleen maar verkeerd gelezen kan worden. Waar hij WEL aanwezig is, staan de genoemde rijen op nul en is attributable false. Het leesmoment is echt, de lezer is een van de mensen op het bericht, en "iemand op dit bericht" is de enige weergave die de data ondersteunt. Vul de naam nooit in vanuit de ontvangerslijst.
Percentages over een venster
{ "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 }] }De percentages zijn percentages over GETRACKTE berichten, niet over alle verzonden post: een workspace die één bericht op de tien trackt heeft een openingspercentage over die tien, en delen door alles wat hij ooit verstuurde zou dalen elke keer dat iemand een ongetrackt antwoord verstuurde. Een bericht dat vijf keer is geopend is ÉÉN geopend bericht. De percentages tellen berichten en de totalen tellen hits, en die twee door elkaar halen is hoe openingspercentages boven 100% gepubliceerd worden.
byDay is dun bezet: een dag waarop niets is getrackt ontbreekt in plaats van op nul te staan, dus vul de gaten op voordat je het in een grafiek zet. Dagen worden gegroepeerd op offsetMinutes ten oosten van UTC (−840 tot 840), zodat ze breken waar de dag van de lezer breekt. medianTimeToOpenSeconds is een mediaan en geen gemiddelde, want één bericht dat drie weken te laat wordt geopend trekt een gemiddelde naar een plek waar geen enkel bericht zit.
De afzonderlijke hits
{ "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 is human, proxy of machine, en counted zegt of het de cijfers heeft bewogen. Machinehits worden uitgesloten tenzij je includeMachine=true meegeeft, wat de eerlijke standaard is: ze worden vastgelegd omdat ze weglaten een onverklaarbaar gat zou achterlaten, niet omdat ze betrokkenheid zijn.
De locatie is grof omdat dat alles is wat er is. Er wordt voor geen enkele hit een IP-adres opgeslagen. Het land, de regio en de stad zijn wat de edge al wist, en de enige andere identificatie die wordt bewaard is een hash waarvan de salt dagelijks roteert, zodat hij binnen een dag twee ophaalacties uit elkaar kan houden en de dag erna inert is.
Wat de cijfers niet kunnen zeggen
- Apple Mail Privacy Protection haalt bij bezorging elke afbeelding in elk bericht op, of er nu iemand kijkt of niet. Het wordt geclassificeerd op basis van de User-Agent en het netwerk en vastgelegd als
machine, net als alles wat binnen tien seconden na de verzending binnenkomt, want niets wat een mens doet gaat zo snel. - De afbeeldingsproxy van Gmail is
proxyen geenmachine: iemand heeft het bericht weergegeven, dus de opening is echt, terwijl het apparaat, de client en de locatie niet te achterhalen zijn. De proxy cachet ook, dus een tweede leesmoment bereikt ons mogelijk nooit. Tellingen via Gmail zijn een ondergrens, nooit een totaal. - Twee ophaalacties van dezelfde kopie binnen dertig seconden zijn één leesmoment. Een voorbeeldvenster dat opnieuw tekent of een bericht dat weer in beeld wordt gescrold haalt de afbeelding opnieuw op; het echte tweede bezoek een uur later wordt nog steeds geteld.
- De ontvanger bij naam noemen vereist een bericht dat klein genoeg is om per persoon opnieuw op te bouwen: de geschatte grootte maal het aantal ontvangers moet onder 8MB blijven. Daarboven gaat één body naar iedereen, en elke hit daarop is niet toegewezen.
- Een bericht met kliks en zonder openingen is zeker gelezen: afbeeldingen worden veel vaker geblokkeerd dan dat links onaangeklikt blijven. Lees de twee tellers apart in plaats van ze op te tellen.
- Kliks vragen op een body zonder links legt helemaal niets vast: de bytes die de deur uit gingen zijn identiek aan een ongetrackte verzending, en een rij die het tegendeel beweert valt nergens mee te rijmen. Hetzelfde geldt voor een bericht zonder body om te herschrijven.
- OpenEmail verwijdert 1×1-afbeeldingen uit de post die zijn eigen gebruikers lezen, inclusief de pixel die het zelf verstuurt, en legt de opening zelf vast wanneer een bericht met zichtbare afbeeldingen wordt weergegeven. Die hit is
humanmet als clientOpenEmail. Met verborgen afbeeldingen wordt er niets vastgelegd.
GET /tracking/{id} en GET /emails/{id}/tracking antwoorden met 404 voor een bericht dat nooit is getrackt, in plaats van met een leeg rapport. De zinnen "we hebben niets vastgelegd" en "niemand heeft het geopend" zijn verschillende antwoorden en mogen geen respons delen. Het lijst-endpoint bevat alleen getrackte berichten, dus een ongetrackt bericht ontbreekt daar simpelweg in plaats van erin te staan met nullen.
Verteld worden in plaats van vragen
Een getelde opening vuurt email.opened af en een getelde klik vuurt email.clicked af naar elk geabonneerd endpoint, en beide worden weggeschreven naar het eigen gebeurtenissenspoor van het bericht wanneer het via deze API is gegaan. Geen van beide vuurt af voor een scanner of een privacyproxy. Die doorsturen zou het logboek van een ontvanger vullen met precies het verkeer dat de classifier uit de cijfers moet houden.
Een bestand dat als downloadlink is meegegaan rapporteert op dezelfde manier. Een getelde download vuurt email.downloaded af en komt op hetzelfde spoor terecht, en dezelfde classifier houdt scanners en linkvoorbeelden eruit, zodat de telling mensen betreft. De payload noemt het bestand (shareId, fileId, filename, mimeType, sizeBytes, url) met downloadCount, first en downloadedAt naast de client- en locatievelden die een klik draagt. recipient is altijd null en attributed altijd false: een downloadlink is één URL voor elke ontvanger van het bericht, dus een download kan niet aan een van hen worden gekoppeld.
Vanuit de 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(),})Elke aanroep hier is een gewone leesactie, en de client probeert elke aanroep zelf opnieuw. get gooit een OpenEmailApiError waarvan isNotFound true is voor een bericht dat nooit is getrackt, en dat is het onderscheid dat het waard is te behouden in waar je het ook in verwerkt.