Zur Dokumentation springen
API

Öffnungs- und Klick-Tracking

GET /tracking: ob eine Nachricht gelesen wurde und welchen Links gefolgt wurde.

GETapi.openemail.uk/emails/{id}/tracking

Führt jeden der 6 Aufrufe auf dieser Seite gegen Ihren Workspace aus, mit Ihrem eigenen Schlüssel.

Was aufgezeichnet wird

Zwei unabhängige Schalter, beide an, sofern sie nicht für die Adresse, von der eine Nachricht gesendet wird, oder für Alle Adressen abgeschaltet wurden. opens hängt ein 1×1-Bild an; clicks schreibt die Links im neuen Teil des Textkörpers um. Die zitierte Historie unter einer Antwort ist die Nachricht eines anderen und bleibt unangetastet. Ein Versand nennt tracking: { opens, clicks }, um für eine einzelne Nachricht zu entscheiden (in beide Richtungen, sodass false die Art ist, in der ein Programm ablehnt, was für die Adresse eingestellt ist), und ein Feld, das Sie weglassen, fällt auf die Einstellung der Absenderadresse zurück, dann auf Alle Adressen, statt auf einen Standard, den diese API stellvertretend für einen Workspace gewählt hätte.

POST /emails
{    "from": "Acme Billing <[email protected]>",    "to": ["[email protected]"],    "subject": "Your September invoice",    "html": "<p>Invoice attached.</p>",    "tracking": { "opens": true, "clicks": true }  }

Pro Nachricht werden höchstens 100 Ziele umgeschrieben, jedes einmal. Dieselbe URL, verlinkt aus einem Header-Bild, einem Button und einer Fußzeile, ist eine Zeile, denn es ist eine Frage, dreimal gestellt. Oberhalb der Grenze bleiben die restlichen Links genau so, wie sie geschrieben wurden: Ein ungetrackter Link funktioniert weiterhin, und eine Nachricht, die stillschweigend ihre letzten zweihundert Links verliert, ist ein weit schlimmerer Fehler als ein unvollständiger Bericht.

Umgeschriebene Links und das Pixel zeigen standardmäßig auf den OpenEmail-API-Host. Hat die sendende Domain eine eigene Tracking-Domain, deren tracking.status active ist, verwendet neue Post von dieser Domain stattdessen https://<tracking host>/t/..., und PATCH /domains/{id} ist der Ort, an dem Sie eine festlegen.

All das benötigt emails:read, und einen Tracking-Scope gibt es nicht. Dieser Scope bedeutet ohnehin schon "gesendete Nachrichten und deren Zustellstatus lesen", und ob jemand eine Nachricht geöffnet hat, ist der denkbar wörtlichste Zustellstatus.

Die Endpunkte

AufrufGibt zurück
`GET /tracking`Getrackte Nachrichten, neueste zuerst. opened, clicked, days (1–365, Standard 30), limit (max. 200).
`GET /tracking/stats`Raten über ein Zeitfenster. days (Standard 30) und offsetMinutes, damit Tage dort enden, wo der Tag des Lesers endet.
`GET /tracking/{id}`Ein einzelner Bericht. Nimmt eine tmsg_-Tracking-ID oder die msg_-ID, die ein Versand zurückgegeben hat.
`GET /tracking/{id}/opens`Die einzelnen Abrufe. includeMachine, limit (max. 200).
`GET /tracking/{id}/clicks`Dasselbe, mit linkId und url in jeder Zeile.
`GET /emails/{id}/tracking`Derselbe Bericht, ausgehend von der Versand-ID, die Sie bereits haben.

Booleans werden im Query-String ausgeschrieben: true, false, 1 oder 0, alles andere wird abgelehnt. Boolean("false") ist true, sodass ein gecastetes ?opened=false genau das Gegenteil dessen zurückgäbe, wonach gefragt wurde.

Dies ist eine eigene Ressource statt ein paar Felder an /emails, und zwar wegen der Abdeckung: Jene Liste enthält Versanddatensätze, und der Composer, die MCP-Tools und der Assistent senden alle, ohne einen zu schreiben. Ein darauf aufgebauter Bericht wäre ein Bericht über Ihren API-Verkehr statt über das Postfach.

Der Bericht

GET /tracking/tmsg_9c1f7b2e4a5d40b8a3e61d2f
{    "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 und clicks sind das, was auf die Nachricht ANGEWENDET wurde; opened und clicked sind das, was geschehen ist. openCount zählt Lesevorgänge, openCountRaw zählt Abrufe. Die Differenz, hier vier, sind die Scanner und die Datenschutz-Proxys; sie wird behalten, damit die Lücke zwischen Protokoll und Summe nachvollziehbar statt unerklärt ist. attributable ist das Feld, das man liest, bevor man jemanden benennt: false heißt, dass ein Lesevorgang auf einer Kopie gelandet ist, die an die gesamte Liste ging, und jeder Satz über einen bestimmten Empfänger ist danach geraten.

source benennt die Oberfläche, die gesendet hat: api für einen Versand über diese API, composer für alles, was die App selbst gesendet hat. sendId ist bei der zweiten Art null, weshalb es die Tracking-ID gibt.

Eine Zeile mit einem email von null und attributed: false ist der Ort, an dem ein Lesevorgang landet, der keiner Person zugeordnet werden konnte, und ein Bericht zeigt eine solche nur, wenn tatsächlich gelesen wurde. Eine Nachricht mit einem einzigen Empfänger hat gar keine, denn ein Textkörper und ein Adressat sind dieselbe Aussage. Eine Nachricht mit mehreren hat vom Moment des Versands an eine im Hintergrund, weil der Transport bis zum Dispatch nicht feststeht, und sie bleibt aus dem Bericht, bis etwas darauf eintrifft: Ein dauerhaftes „jemand: nicht geöffnet“ neben den namentlich genannten Empfängern ist eine Zeile, die nur missverstanden werden kann. Wo sie VORHANDEN ist, stehen die namentlichen Zeilen auf null und attributable ist false. Der Lesevorgang ist echt, der Lesende ist eine der Personen auf der Nachricht, und „jemand auf dieser Nachricht“ ist die einzige Darstellung, die die Daten hergeben. Füllen Sie den Namen niemals aus der Empfängerliste auf.

Raten über ein Zeitfenster

GET /tracking/stats?days=30&offsetMinutes=60
{    "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 }]  }

Die Raten sind Prozentsätze über GETRACKTE Nachrichten, nicht über alle gesendete Post: Ein Workspace, der eine von zehn Nachrichten trackt, hat eine Öffnungsrate für diese zehn, und durch alles Jemals-Gesendete zu teilen würde sie jedes Mal fallen lassen, wenn jemand eine ungetrackte Antwort schickt. Eine fünfmal geöffnete Nachricht ist EINE geöffnete Nachricht. Die Raten zählen Nachrichten und die Summen zählen Treffer; beides zu vermengen ist der Weg, auf dem Öffnungsraten über 100 % veröffentlicht werden.

byDay ist dünn besetzt: Ein Tag, an dem nichts getrackt wurde, fehlt, statt null zu sein – füllen Sie die Lücken also, bevor Sie es als Diagramm darstellen. Tage werden bei offsetMinutes östlich von UTC (−840 bis 840) gebucketet, damit sie dort enden, wo der Tag des Lesers endet. medianTimeToOpenSeconds ist ein Median und kein Mittelwert, denn eine Nachricht, die drei Wochen später geöffnet wird, zieht einen Durchschnitt irgendwohin, wo keine Nachricht tatsächlich liegt.

Die einzelnen Treffer

GET /tracking/tmsg_…/opens?includeMachine=true
{    "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 ist human, proxy oder machine, und counted sagt, ob der Treffer die Zahlen bewegt hat. Maschinelle Treffer werden ausgeschlossen, sofern Sie nicht includeMachine=true übergeben, was der ehrliche Standard ist: Sie werden erfasst, weil ihr Weglassen eine unerklärliche Lücke hinterließe, nicht weil sie Engagement wären.

Die Ortsangabe ist grob, weil es mehr nicht gibt. Zu keinem Treffer wird eine IP-Adresse gespeichert. Land, Region und Stadt sind das, was die Edge ohnehin wusste, und der einzige weitere gespeicherte Identifikator ist ein Hash, dessen Salt täglich rotiert – er kann zwei Abrufe innerhalb eines Tages unterscheiden und ist am Tag darauf wirkungslos.

Was die Zahlen nicht sagen können

  • Apple Mail Privacy Protection ruft bei der Zustellung jedes Bild in jeder Nachricht ab, unabhängig davon, ob jemand hinsieht. Es wird anhand des User-Agent und des Netzwerks klassifiziert und als machine erfasst, ebenso alles, was innerhalb von zehn Sekunden nach dem Versand eintrifft, denn nichts, was ein Mensch tut, geschieht so schnell.
  • Gmails Bild-Proxy ist proxy und nicht machine: Jemand hat die Nachricht angezeigt, die Öffnung ist also echt, während Gerät, Client und Ort nicht ermittelbar sind. Der Proxy cacht zudem, sodass ein zweiter Lesevorgang uns womöglich nie erreicht. Zahlen über Gmail sind eine Untergrenze, nie eine Gesamtsumme.
  • Zwei Abrufe derselben Kopie innerhalb von dreißig Sekunden sind ein Lesevorgang. Ein neu gezeichnetes Vorschaufenster oder eine wieder ins Bild gescrollte Nachricht ruft das Bild erneut ab; der echte zweite Besuch eine Stunde später wird weiterhin gezählt.
  • Den Empfänger zu benennen erfordert eine Nachricht, die klein genug ist, um sie pro Person neu aufzubauen: Die geschätzte Größe mal der Empfängeranzahl muss unter 8 MB bleiben. Darüber geht ein Textkörper an alle, und jeder Treffer darauf ist nicht zugeordnet.
  • Eine Nachricht mit Klicks und ohne Öffnungen wurde mit Sicherheit gelesen: Bilder werden weit häufiger blockiert, als Links ungeklickt bleiben. Lesen Sie die beiden Zähler getrennt, statt sie zu addieren.
  • Klicks für einen Textkörper ohne Links anzufordern zeichnet überhaupt nichts auf: Die versendeten Bytes sind identisch mit einem ungetrackten Versand, und eine Zeile, die etwas anderes behauptet, ließe sich mit nichts abgleichen. Dasselbe gilt für eine Nachricht ohne Textkörper, der umgeschrieben werden könnte.
  • OpenEmail entfernt 1×1-Bilder aus der Post, die seine eigenen Nutzer lesen, das eigene Pixel eingeschlossen, und erfasst die Öffnung selbst, wenn eine Nachricht mit sichtbaren Bildern angezeigt wird. Dieser Treffer ist human mit dem Client OpenEmail. Bei ausgeblendeten Bildern wird nichts erfasst.

GET /tracking/{id} und GET /emails/{id}/tracking antworten für eine nie getrackte Nachricht mit 404 statt mit einem leeren Bericht. Die Sätze "wir haben nichts erfasst" und "niemand hat sie geöffnet" sind verschiedene Antworten und dürfen sich keine Response teilen. Der Listen-Endpunkt enthält nur getrackte Nachrichten, sodass eine ungetrackte darin schlicht fehlt, statt mit Nullen aufzutauchen.

Benachrichtigt werden, statt zu fragen

Eine gezählte Öffnung löst email.opened aus und ein gezählter Klick email.clicked, an jedem abonnierten Endpunkt, und beide werden in den eigenen Ereignisverlauf der Nachricht geschrieben, sofern sie über diese API lief. Für einen Scanner oder einen Datenschutz-Proxy löst keines von beiden aus. Diese zu pushen würde das Log eines Empfängers mit genau dem Verkehr füllen, den der Klassifikator aus den Zahlen heraushalten soll.

Eine Datei, die als Download-Link hinausging, wird auf die gleiche Weise berichtet. Ein gezählter Download löst email.downloaded aus und landet im selben Verlauf, und derselbe Klassifikator hält Scanner und Link-Vorschauen davon fern, sodass die Zahl Menschen bedeutet. Die Payload benennt die Datei (shareId, fileId, filename, mimeType, sizeBytes, url) mit downloadCount, first und downloadedAt neben den Client- und Ortsfeldern, die ein Klick mitführt. recipient ist immer null und attributed immer false: Ein Download-Link ist eine URL für jeden Empfänger der Nachricht, ein Download lässt sich also keinem von ihnen zuordnen.

Über das SDK

openemail.tracking
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(),})

Jeder Aufruf hier ist ein reiner Lesevorgang, und der Client wiederholt jeden einzeln. get wirft einen OpenEmailApiError, dessen isNotFound bei einer nie getrackten Nachricht true ist – das ist die Unterscheidung, die es zu bewahren lohnt, wohin auch immer Sie das weiterreichen.