Saltar para a documentação
API

Rastreio de aberturas e cliques

GET /tracking: se uma mensagem foi lida, e o que foi seguido.

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

Executa qualquer uma das 6 chamadas desta página contra o seu espaço de trabalho, com a sua própria chave.

O que é registado

Dois interruptores independentes, ambos ligados a não ser que tenham sido desligados para o endereço a partir do qual a mensagem é enviada ou para Todos os endereços. opens acrescenta uma imagem de 1×1; clicks reescreve as ligações na parte nova do corpo. O histórico citado por baixo de uma resposta é a mensagem de outra pessoa e é deixado em paz. Um envio nomeia tracking: { opens, clicks } para decidir por uma mensagem (em qualquer dos sentidos, pelo que false é como um programa recusa aquilo que o endereço está configurado para fazer), e um campo que omita recai sobre a definição do endereço a partir do qual é enviada, depois sobre Todos os endereços, em vez de sobre um valor que esta API escolhesse em nome de um workspace.

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

São reescritos no máximo 100 destinos por mensagem, uma vez cada. O mesmo URL ligado a partir de uma imagem de cabeçalho, de um botão e de um rodapé é uma só linha, porque é uma só pergunta feita três vezes. Passado o limite, as ligações restantes ficam exatamente como foram escritas: uma ligação não rastreada continua a funcionar, e uma mensagem que perde em silêncio as últimas duzentas ligações é uma falha muito pior do que um relatório incompleto.

As ligações reescritas e o pixel apontam por omissão para o host da API do OpenEmail. Quando o domínio de envio tem um domínio de rastreio personalizado cujo tracking.status é active, o correio novo desse domínio usa https://<tracking host>/t/..., e é em PATCH /domains/{id} que se define um.

Tudo isto precisa de emails:read, e não existe um âmbito de rastreio. Esse âmbito já significa «ler mensagens enviadas e o seu estado de entrega», e saber se alguém abriu uma mensagem é o estado de entrega mais literal que há.

Os endpoints

ChamadaDevolve
`GET /tracking`Mensagens rastreadas, das mais recentes para as mais antigas. opened, clicked, days (1–365, 30 por omissão), limit (máx. 200).
`GET /tracking/stats`Taxas ao longo de uma janela. days (30 por omissão) e offsetMinutes, para que os dias quebrem onde quebra o dia de quem lê.
`GET /tracking/{id}`Um relatório. Aceita um id de rastreio tmsg_ ou o id msg_ que um envio devolveu.
`GET /tracking/{id}/opens`As obtenções individuais. includeMachine, limit (máx. 200).
`GET /tracking/{id}/clicks`O mesmo, com linkId e url em cada linha.
`GET /emails/{id}/tracking`O mesmo relatório, a partir do id de envio que já tem.

Os booleanos escrevem-se por extenso na query string: true, false, 1 ou 0, e tudo o resto é recusado. Boolean("false") é true, pelo que um ?opened=false convertido devolveria exatamente o oposto do que foi pedido.

Isto é um recurso próprio em vez de uns quantos campos em /emails por causa da cobertura: essa lista contém registos de envio, e o compositor, as ferramentas MCP e o assistente enviam todos sem escrever um. Um relatório construído sobre ela seria um relatório sobre o seu tráfego de API e não sobre a caixa de correio.

O relatório

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 e clicks são o que foi APLICADO à mensagem; opened e clicked são o que aconteceu. openCount conta leituras e openCountRaw conta obtenções. A diferença, aqui de quatro, são os scanners e os proxies de privacidade, mantida para que a distância entre o registo e o total seja inspecionável em vez de inexplicada. attributable é o campo a ler antes de nomear seja quem for: false significa que uma leitura aterrou numa cópia que foi para toda a lista, e a partir daí qualquer frase sobre um destinatário em concreto é um palpite.

source nomeia a superfície que a enviou: api para um envio através desta API, composer para tudo o que a própria aplicação enviou. sendId é null no segundo caso, e é por isso que o id de rastreio existe.

Uma linha com email a null e attributed: false é onde aterra uma leitura que não pôde ser associada a uma pessoa, e um relatório só mostra uma quando houve mesmo uma leitura dessas. Uma mensagem com um único destinatário não tem nenhuma, porque um corpo e um destinatário são a mesma afirmação. Uma mensagem com vários tem uma por trás desde o momento em que saiu, porque o transporte só assenta no despacho, e ela fica fora do relatório até que algo lhe chegue: um permanente «alguém: não abriu» ao lado dos destinatários nomeados é uma linha que só pode ser mal lida. Onde ELA ESTÁ presente, as linhas nomeadas são as que ficam a zero e attributable é false. A leitura é real, o leitor é uma das pessoas na mensagem, e «alguém nesta mensagem» é a única leitura que os dados sustentam. Nunca preencha o nome a partir da lista de destinatários.

Taxas ao longo de uma janela

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

As taxas são percentagens sobre as mensagens RASTREADAS, e não sobre todo o correio enviado: um workspace que rastreia uma mensagem em cada dez tem uma taxa de abertura para essas dez, e dividir por tudo o que alguma vez enviou faria a taxa cair sempre que alguém enviasse uma resposta não rastreada. Uma mensagem aberta cinco vezes é UMA mensagem aberta. As taxas contam mensagens e os totais contam acessos, e confundir as duas coisas é como se publicam taxas de abertura acima de 100%.

byDay é esparso: um dia em que nada foi rastreado está ausente em vez de a zero, por isso preencha os intervalos antes de o levar a um gráfico. Os dias são agrupados a offsetMinutes a leste de UTC (−840 a 840), para que quebrem onde quebra o dia de quem lê. medianTimeToOpenSeconds é uma mediana e não uma média, porque uma mensagem aberta três semanas depois arrasta uma média para um sítio onde nenhuma mensagem está.

Os acessos individuais

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 é human, proxy ou machine, e counted diz se mexeu nos números. Os acessos de máquina são excluídos a não ser que passe includeMachine=true, que é o comportamento honesto por omissão: são registados porque descartá-los deixaria um vazio inexplicável, não porque sejam interação.

A localização é grosseira porque é tudo o que há. Não é guardado nenhum endereço IP para acesso nenhum. O país, a região e a cidade são o que a edge já sabia, e o único outro identificador guardado é um hash cujo sal roda diariamente, pelo que consegue distinguir duas obtenções dentro do mesmo dia e fica inerte no dia seguinte.

O que os números não conseguem dizer

  • A Apple Mail Privacy Protection obtém todas as imagens de todas as mensagens na entrega, olhe alguém para elas ou não. É classificada a partir do User-Agent e da rede e registada como machine, tal como tudo o que chegue nos dez segundos seguintes ao envio, porque nada que uma pessoa faça acontece tão depressa.
  • O proxy de imagens do Gmail é proxy e não machine: alguém apresentou a mensagem, pelo que a abertura é real, ainda que o dispositivo, o cliente e a localização não sejam cognoscíveis. O proxy também guarda em cache, pelo que uma segunda leitura pode nunca chegar até nós. As contagens através do Gmail são um mínimo, nunca um total.
  • Duas obtenções da mesma cópia dentro de trinta segundos são uma leitura. Um painel de pré-visualização a redesenhar ou uma mensagem que volta a entrar no ecrã voltam a obter a imagem; a segunda visita genuína uma hora depois continua a ser contada.
  • Nomear o destinatário exige uma mensagem pequena o suficiente para ser reconstruída por pessoa: o tamanho estimado a multiplicar pelo número de destinatários tem de ficar abaixo de 8MB. Acima disso, um só corpo vai para toda a gente, e todos os acessos a ele ficam sem atribuição.
  • Uma mensagem com cliques e sem aberturas foi de certeza lida: as imagens são bloqueadas muito mais vezes do que as ligações ficam por clicar. Leia os dois contadores em separado, em vez de os somar.
  • Pedir cliques num corpo sem ligações não regista absolutamente nada: os bytes que saíram são idênticos aos de um envio não rastreado, e uma linha que dissesse o contrário não poderia ser reconciliada com coisa nenhuma. O mesmo vale para uma mensagem sem corpo para reescrever.
  • O OpenEmail retira as imagens de 1×1 do correio que os seus próprios utilizadores leem, incluindo o pixel que ele envia, e regista ele próprio a abertura quando uma mensagem é apresentada com as imagens visíveis. Esse acesso é human com o cliente OpenEmail. Com as imagens ocultas não é registado nada.

GET /tracking/{id} e GET /emails/{id}/tracking respondem 404 para uma mensagem que nunca foi rastreada, em vez de um relatório vazio. As frases «não registámos nada» e «ninguém a abriu» são respostas diferentes e não podem partilhar uma resposta. O endpoint de lista contém apenas mensagens rastreadas, pelo que uma não rastreada está simplesmente ausente dele, em vez de presente com zeros.

Ser avisado em vez de perguntar

Uma abertura contada dispara email.opened e um clique contado dispara email.clicked em todos os endpoints subscritos, e ambos são escritos no rasto de eventos da própria mensagem quando ela passou por esta API. Nenhum dispara para um scanner ou um proxy de privacidade. Empurrá-los encheria o registo de quem recebe exatamente com o tráfego que o classificador existe para manter fora dos números.

Um ficheiro que saiu como ligação de transferência reporta da mesma forma. Uma transferência contada dispara email.downloaded e aterra no mesmo rasto, e o mesmo classificador mantém fora dela os scanners e os pré-visualizadores de ligações, pelo que a contagem é de pessoas. O payload nomeia o ficheiro (shareId, fileId, filename, mimeType, sizeBytes, url) com downloadCount, first e downloadedAt ao lado dos campos de cliente e localização que um clique traz. recipient é sempre null e attributed sempre false: uma ligação de transferência é um só URL para todos os destinatários da mensagem, pelo que uma transferência não pode ser associada a um deles.

A partir do 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 as chamadas aqui são leituras simples, e o cliente repete cada uma por si. get lança um OpenEmailApiError cujo isNotFound é true para uma mensagem que nunca foi rastreada, que é a distinção que vale a pena preservar em tudo aquilo para onde a leve.