Ir a la documentación
API

Seguimiento de aperturas y clics

GET /tracking: si un mensaje se leyó, y qué se siguió.

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

Ejecuta cualquiera de las 6 llamadas de esta página contra tu espacio de trabajo, con tu propia clave.

Qué se registra

Dos interruptores independientes, ambos activados salvo que se hayan desactivado para la dirección desde la que se envía un mensaje o para Todas las direcciones. opens añade una imagen de 1×1; clicks reescribe los enlaces de la parte nueva del cuerpo. El historial citado bajo una respuesta es el mensaje de otra persona y no se toca. Un envío nombra tracking: { opens, clicks } para decidir sobre un mensaje concreto (en cualquiera de los dos sentidos, así que false es como un programa rechaza lo que la dirección tiene configurado), y un campo que omites recae en el ajuste de la dirección desde la que se envía, después en Todas las direcciones, y no en un valor por defecto que esta API haya elegido en nombre de un espacio de trabajo.

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

Se reescriben como máximo 100 destinos por mensaje, una vez cada uno. La misma URL enlazada desde una imagen de cabecera, un botón y un pie es una sola fila, porque es una sola pregunta hecha tres veces. Pasado el tope, los enlaces restantes se dejan exactamente como se escribieron: un enlace sin seguimiento sigue funcionando, y un mensaje que pierde en silencio sus últimos doscientos enlaces es un fallo mucho peor que un informe incompleto.

Los enlaces reescritos y el píxel apuntan por defecto al host de la API de OpenEmail. Cuando el dominio remitente tiene un dominio de seguimiento personalizado cuyo tracking.status es active, el correo nuevo de ese dominio usa https://<tracking host>/t/... en su lugar, y PATCH /domains/{id} es donde se configura uno.

Todo esto requiere emails:read, y no hay ningún ámbito de seguimiento. Ese ámbito ya significa «leer mensajes enviados y su estado de entrega», y si alguien abrió un mensaje es el estado de entrega más literal posible.

Los endpoints

LlamadaDevuelve
`GET /tracking`Mensajes con seguimiento, del más reciente al más antiguo. opened, clicked, days (1–365, predeterminado 30), limit (máx. 200).
`GET /tracking/stats`Tasas dentro de una ventana. days (predeterminado 30) y offsetMinutes, para que los días se corten donde se corta el día del lector.
`GET /tracking/{id}`Un solo informe. Acepta un id de seguimiento tmsg_ o el id msg_ que devolvió un envío.
`GET /tracking/{id}/opens`Las solicitudes individuales. includeMachine, limit (máx. 200).
`GET /tracking/{id}/clicks`Lo mismo, con linkId y url en cada fila.
`GET /emails/{id}/tracking`El mismo informe, a partir del id de envío que ya tienes.

Los booleanos se escriben literalmente en la cadena de consulta: true, false, 1 o 0; cualquier otro valor se rechaza. Boolean("false") es true, así que un ?opened=false convertido por coerción devolvería exactamente lo contrario de lo que se pidió.

Es un recurso propio y no unos cuantos campos en /emails por una cuestión de cobertura: esa lista contiene registros de envío, y el redactor, las herramientas MCP y el asistente envían sin escribir ninguno. Un informe construido sobre ella sería un informe sobre tu tráfico de API y no sobre el buzón.

El informe

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 y clicks son lo que se APLICÓ al mensaje; opened y clicked son lo que ocurrió. openCount cuenta lecturas y openCountRaw cuenta solicitudes. La diferencia, aquí de cuatro, son los escáneres y los proxies de privacidad, que se conservan para que la brecha entre el registro y el total sea inspeccionable en lugar de inexplicable. attributable es el campo que hay que leer antes de nombrar a nadie: false significa que una lectura cayó sobre una copia que se envió a toda la lista, y a partir de ahí cualquier afirmación sobre un destinatario concreto es una suposición.

source nombra la superficie que lo envió: api para un envío a través de esta API, composer para todo lo que envió la propia aplicación. sendId es null en el segundo caso, y por eso existe el id de seguimiento.

Una fila con email en null y attributed: false es donde cae una lectura que no se pudo atribuir a una persona, y un informe solo la muestra cuando una lectura efectivamente cayó ahí. Un mensaje con un único destinatario no tiene ninguna, porque un cuerpo y un destinatario son la misma afirmación. Un mensaje con varios la tiene detrás desde el momento en que salió, porque el transporte no queda decidido hasta el despacho, y se mantiene fuera del informe hasta que algo llega a ella: un permanente «alguien: no abierto» junto a los destinatarios nombrados es una fila que solo se puede malinterpretar. Cuando SÍ está presente, las filas nombradas son las que están en cero y attributable es false. La lectura es real, el lector es una de las personas del mensaje, y «alguien de este mensaje» es la única representación que los datos admiten. Nunca completes el nombre a partir de la lista de destinatarios.

Tasas dentro de una ventana

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 }]  }

Las tasas son porcentajes sobre los mensajes CON SEGUIMIENTO, no sobre todo el correo enviado: un espacio de trabajo que hace seguimiento de uno de cada diez mensajes tiene una tasa de apertura para esos diez, y dividir entre todo lo que ha enviado haría caer la cifra cada vez que alguien respondiera sin seguimiento. Un mensaje abierto cinco veces es UN mensaje abierto. Las tasas cuentan mensajes y los totales cuentan impactos; confundir ambas cosas es como se acaban publicando tasas de apertura superiores al 100 %.

byDay es disperso: un día en el que no se registró nada está ausente en lugar de valer cero, así que rellena los huecos antes de graficarlo. Los días se agrupan a offsetMinutes al este de UTC (−840 a 840) para que se corten donde se corta el día del lector. medianTimeToOpenSeconds es una mediana y no una media, porque un mensaje abierto con tres semanas de retraso arrastra el promedio a un punto donde no está ningún mensaje.

Los impactos individuales

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 es human, proxy o machine, y counted indica si movió las cifras. Los impactos de máquina se excluyen salvo que pases includeMachine=true, que es el valor predeterminado honesto: se registran porque descartarlos dejaría un hueco inexplicable, no porque sean interacción real.

La ubicación es aproximada porque es todo lo que hay. No se almacena ninguna dirección IP de ningún impacto. El país, la región y la ciudad son lo que el edge ya sabía, y el único otro identificador que se conserva es un hash cuyo salt rota a diario, de modo que puede distinguir dos solicitudes dentro de un mismo día y queda inerte al día siguiente.

Lo que las cifras no pueden decir

  • Apple Mail Privacy Protection descarga todas las imágenes de todos los mensajes en el momento de la entrega, mire alguien o no. Se clasifica a partir del User-Agent y de la red y se registra como machine, igual que cualquier cosa que llegue dentro de los diez segundos posteriores al envío, porque nada de lo que hace una persona ocurre tan rápido.
  • El proxy de imágenes de Gmail es proxy y no machine: alguien mostró el mensaje, así que la apertura es real, aunque el dispositivo, el cliente y la ubicación no se puedan conocer. El proxy además almacena en caché, así que una segunda lectura puede que nunca nos llegue. Los recuentos a través de Gmail son un mínimo, nunca un total.
  • Dos solicitudes de la misma copia dentro de treinta segundos son una sola lectura. Un panel de vista previa que se redibuja o un mensaje que vuelve a entrar en pantalla al desplazarse vuelven a pedir la imagen; la segunda visita genuina una hora después sí se cuenta.
  • Nombrar al destinatario exige un mensaje lo bastante pequeño como para reconstruirlo por persona: el tamaño estimado multiplicado por el número de destinatarios debe quedar por debajo de 8MB. Por encima de eso, un mismo cuerpo va a todos, y cada impacto sobre él queda sin atribuir.
  • Un mensaje con clics y sin aperturas se ha leído con seguridad: las imágenes se bloquean mucho más a menudo de lo que los enlaces quedan sin pulsar. Lee los dos contadores por separado en lugar de sumarlos.
  • Pedir seguimiento de clics en un cuerpo sin enlaces no registra absolutamente nada: los bytes que salieron son idénticos a los de un envío sin seguimiento, y una fila que afirmara lo contrario no se podría conciliar con nada. Lo mismo ocurre con un mensaje sin cuerpo que reescribir.
  • OpenEmail elimina las imágenes de 1×1 del correo que leen sus propios usuarios, incluido el píxel que envía, y registra la apertura por su cuenta cuando un mensaje se muestra con las imágenes visibles. Ese impacto es human con el cliente OpenEmail. Con las imágenes ocultas no se registra nada.

GET /tracking/{id} y GET /emails/{id}/tracking responden 404 para un mensaje al que nunca se le hizo seguimiento, en lugar de un informe vacío. Las frases «no registramos nada» y «nadie lo abrió» son respuestas distintas y no deben compartir una misma respuesta. El endpoint de listado solo contiene mensajes con seguimiento, así que uno sin seguimiento simplemente no aparece en él, en vez de aparecer con ceros.

Que te avisen en lugar de preguntar

Una apertura contabilizada dispara email.opened y un clic contabilizado dispara email.clicked en todos los endpoints suscritos, y ambos se escriben en el propio rastro de eventos del mensaje cuando este pasó por esta API. Ninguno se dispara por un escáner o un proxy de privacidad. Enviarlos llenaría el registro del receptor exactamente con el tráfico que el clasificador existe para mantener fuera de las cifras.

Un archivo que salió como enlace de descarga informa de la misma manera. Una descarga contabilizada dispara email.downloaded y llega al mismo rastro, y el mismo clasificador mantiene fuera a los escáneres y a los generadores de vistas previas de enlaces, de modo que el recuento son personas. El payload nombra el archivo (shareId, fileId, filename, mimeType, sizeBytes, url) junto con downloadCount, first y downloadedAt, además de los campos de cliente y ubicación que lleva un clic. recipient siempre es null y attributed siempre false: un enlace de descarga es una sola URL para todos los destinatarios del mensaje, así que una descarga no se puede atribuir a ninguno en concreto.

Desde el 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(),})

Todas las llamadas de aquí son lecturas simples, y el cliente reintenta cada una por su cuenta. get lanza un OpenEmailApiError cuyo isNotFound es true para un mensaje al que nunca se le hizo seguimiento, y esa es la distinción que conviene preservar en aquello a lo que lo conectes.