Перейти к документации
API

Отслеживание открытий и переходов

GET /tracking: было ли сообщение прочитано и по каким ссылкам перешли.

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

Выполняет любой из 6 запросов на этой странице в вашем рабочем пространстве, с вашим собственным ключом.

Что записывается

Два независимых переключателя, оба включены, если только их не выключили для адреса, с которого отправляется сообщение, или для All addresses. opens добавляет изображение 1×1; clicks переписывает ссылки в новой части тела. Цитируемая история под ответом — чужое сообщение, и её не трогают. Отправка указывает tracking: { opens, clicks }, чтобы решить за одно сообщение (в любую сторону, так что false — это способ для программы отказаться от того, что настроено на адресе), а опущенное поле откатывается к настройке адреса, с которого идёт отправка, затем к All addresses, а не к значению по умолчанию, которое этот API выбрал бы за рабочее пространство.

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

Переписывается не более 100 адресов назначения на сообщение, каждый по одному разу. Один и тот же URL, на который ссылаются картинка в шапке, кнопка и подвал, — это одна строка, потому что это один вопрос, заданный трижды. Сверх лимита остальные ссылки остаются ровно такими, как были написаны: неотслеживаемая ссылка всё равно работает, а сообщение, молча теряющее последние двести ссылок, — куда худший сбой, чем неполный отчёт.

Переписанные ссылки и пиксель по умолчанию указывают на хост API OpenEmail. Если у отправляющего домена есть собственный tracking-домен, у которого tracking.status равен active, новые письма с этого домена используют вместо него https://<tracking host>/t/..., а задаётся такой домен через PATCH /domains/{id}.

Всё это требует emails:read, и отдельного scope для трекинга нет. Этот scope уже означает «читать отправленные сообщения и их статус доставки», а открыл ли кто-то сообщение — это самый буквальный статус доставки, какой только возможен.

Эндпоинты

ВызовВозвращает
`GET /tracking`Отслеживаемые сообщения, от новых к старым. opened, clicked, days (1–365, по умолчанию 30), limit (максимум 200).
`GET /tracking/stats`Доли за период. days (по умолчанию 30) и offsetMinutes, чтобы сутки разделялись там же, где они разделяются у читателя.
`GET /tracking/{id}`Один отчёт. Принимает tracking-id вида tmsg_ или id вида msg_, возвращённый отправкой.
`GET /tracking/{id}/opens`Отдельные загрузки. includeMachine, limit (максимум 200).
`GET /tracking/{id}/clicks`То же самое, с linkId и url в каждой строке.
`GET /emails/{id}/tracking`Тот же отчёт, по id отправки, который у вас уже есть.

Булевы значения записываются в строке запроса явно: true, false, 1 или 0, всё остальное отклоняется. Boolean("false") равно true, поэтому приведённый к типу ?opened=false вернул бы ровно противоположное тому, о чём просили.

Это отдельный ресурс, а не несколько полей в /emails, из-за охвата: тот список содержит записи об отправках, а редактор, инструменты MCP и ассистент отправляют, не создавая их. Отчёт, построенный на нём, был бы отчётом о вашем трафике API, а не о почтовом ящике.

Отчёт

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 и clicks — это то, что было ПРИМЕНЕНО к сообщению; opened и clicked — то, что произошло. openCount считает прочтения, а openCountRaw — загрузки. Разница, здесь равная четырём, — это сканеры и прокси приватности, сохраняемые для того, чтобы расхождение между журналом и итогом можно было изучить, а не оставить без объяснения. attributable — поле, которое нужно прочитать, прежде чем кого-либо называть: false означает, что прочтение пришлось на копию, ушедшую всему списку, и всякая фраза о конкретном получателе после этого — догадка.

source называет поверхность, с которой ушло сообщение: api — для отправки через этот API, composer — для всего, что отправило само приложение. Для второго вида sendId равен null — именно поэтому и существует tracking-id.

Строка с email, равным null, и attributed: false — это место, куда попадает прочтение, которое не удалось привязать к человеку, и отчёт показывает такую строку только тогда, когда прочтение действительно туда попало. У сообщения с одним получателем её нет вовсе, потому что одно тело и один адресат — это одно и то же утверждение. У сообщения с несколькими получателями такая строка существует за кулисами с момента отправки, потому что транспорт определяется только при диспетчеризации, и она не попадает в отчёт, пока на неё что-нибудь не придёт: постоянное «кто-то: не открыл» рядом с поимёнными получателями — строка, которую можно только прочитать неверно. Там, где она ЕСТЬ, поимённые строки стоят на нуле, а attributable равно false. Прочтение реально, читатель — один из людей в сообщении, и «кто-то из этого сообщения» — единственная трактовка, которую поддерживают данные. Никогда не подставляйте имя из списка получателей.

Доли за период

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

Доли — это проценты от ОТСЛЕЖИВАЕМЫХ сообщений, а не от всей отправленной почты: у рабочего пространства, которое отслеживает одно сообщение из десяти, доля открытий считается по этим десяти, а деление на всё когда-либо отправленное падало бы каждый раз, когда кто-то отправляет неотслеживаемый ответ. Сообщение, открытое пять раз, — это ОДНО открытое сообщение. Доли считают сообщения, а итоги считают события, и смешение этих двух вещей — то, как публикуются доли открытий выше 100%.

byDay разрежен: день, в который ничего не отслеживалось, отсутствует, а не равен нулю, поэтому перед построением графика заполните пропуски. Дни группируются со смещением offsetMinutes к востоку от UTC (от −840 до 840), чтобы они разделялись там же, где сутки читателя. medianTimeToOpenSeconds — медиана, а не среднее, потому что одно сообщение, открытое через три недели, утаскивает среднее туда, где нет ни одного сообщения.

Отдельные события

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 или machine, а counted говорит, повлияло ли событие на цифры. Машинные события исключаются, если не передать includeMachine=true, и это честное значение по умолчанию: их записывают потому, что их отбрасывание оставило бы необъяснимый пробел, а не потому, что они означают вовлечённость.

Местоположение грубое, потому что другого просто нет. Ни для одного события не хранится IP-адрес. Страна, регион и город — это то, что и так знал edge, а единственный другой сохраняемый идентификатор — хеш, соль которого меняется ежедневно, поэтому он способен отличить две загрузки в пределах суток и бесполезен на следующий день.

Чего цифры сказать не могут

  • Apple Mail Privacy Protection загружает каждое изображение в каждом сообщении при доставке независимо от того, смотрит ли кто-нибудь. Это определяется по User-Agent и сети и записывается как machine, как и всё, что приходит в течение десяти секунд после отправки, потому что ничто из действий человека не происходит так быстро.
  • Прокси изображений Gmail — это proxy, а не machine: сообщение кто-то отобразил, поэтому открытие настоящее, тогда как устройство, клиент и местоположение узнать нельзя. Прокси ещё и кэширует, поэтому второе прочтение может вообще до нас не дойти. Цифры через Gmail — это нижняя граница, а не итог.
  • Две загрузки одной и той же копии в пределах тридцати секунд — это одно прочтение. Перерисовка панели предпросмотра или сообщение, вновь попавшее в область видимости при прокрутке, заново загружают изображение; настоящий второй визит час спустя по-прежнему засчитывается.
  • Чтобы назвать получателя, сообщение должно быть достаточно небольшим для пересборки под каждого: оценочный размер, умноженный на число получателей, должен укладываться в 8MB. Выше этого одно тело уходит всем, и каждое событие по нему остаётся без атрибуции.
  • Сообщение с переходами и без открытий точно было прочитано: изображения блокируют гораздо чаще, чем оставляют ссылки без нажатия. Читайте эти два счётчика по отдельности, а не складывайте их.
  • Запрос отслеживания переходов для тела без ссылок не записывает вообще ничего: ушедшие байты идентичны неотслеживаемой отправке, и строку, утверждающую обратное, было бы не с чем сверить. То же верно для сообщения, в котором нечего переписывать.
  • OpenEmail вырезает изображения 1×1 из писем, которые читают его собственные пользователи, включая отправляемый им самим пиксель, и сам записывает открытие, когда сообщение отображается с показом изображений. Такое событие имеет тип human и клиент OpenEmail. При скрытых изображениях не записывается ничего.

GET /tracking/{id} и GET /emails/{id}/tracking отвечают 404 для сообщения, которое никогда не отслеживалось, а не пустым отчётом. Фразы «мы ничего не записали» и «его никто не открыл» — разные ответы, и у них не должно быть одного представления. Эндпоинт списка содержит только отслеживаемые сообщения, поэтому неотслеживаемое просто отсутствует в нём, а не присутствует с нулями.

Когда сообщают, а не спрашивают

Засчитанное открытие вызывает email.opened, а засчитанный переход — email.clicked на каждом подписанном эндпоинте, и оба записываются в собственную историю событий сообщения, если оно прошло через этот API. Ни то, ни другое не срабатывает для сканера или прокси приватности. Их отправка заполнила бы журнал получателя ровно тем трафиком, который классификатор и существует, чтобы держать вне цифр.

Файл, ушедший ссылкой на скачивание, отчитывается так же. Засчитанное скачивание вызывает email.downloaded и попадает в ту же историю, и тот же классификатор держит вне её сканеры и генераторы превью ссылок, поэтому счётчик считает людей. Полезная нагрузка называет файл (shareId, fileId, filename, mimeType, sizeBytes, url) вместе с downloadCount, first и downloadedAt, рядом с полями клиента и местоположения, которые несёт переход. recipient всегда null, а attributed всегда false: ссылка на скачивание — это один URL для всех получателей сообщения, поэтому скачивание нельзя привязать к одному из них.

Из 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(),})

Каждый вызов здесь — обычное чтение, и клиент повторяет каждый из них самостоятельно. get бросает OpenEmailApiError, у которого isNotFound равен true для сообщения, которое никогда не отслеживалось, — и именно это различие стоит сохранять в том, куда вы это передаёте.