開封とクリックのトラッキング
GET /tracking: メッセージが読まれたか、どのリンクがたどられたか。
このページの6件の呼び出しを、ご自身のキーで自分のワークスペースに対して実行します。
記録される内容
独立した 2 つのスイッチがあり、送信元アドレスまたは「すべてのアドレス」でオフにされていない限り、どちらもオンです。opens は 1×1 の画像を付け、clicks は本文の新しい部分のリンクを書き換えます。返信の下に引用された履歴は他人のメッセージなので、そのまま残されます。送信時に tracking: { opens, clicks } を指定すると 1 通について決められ(どちらの方向にも指定できるので、false はプログラムがそのアドレスの設定を断る方法です)、省略したフィールドは送信元アドレスの設定、次いで「すべてのアドレス」の設定にフォールバックします。この API がワークスペースに代わって選んだ既定値にフォールバックすることはありません。
{ "from": "Acme Billing <[email protected]>", "to": ["[email protected]"], "subject": "Your September invoice", "html": "<p>Invoice attached.</p>", "tracking": { "opens": true, "clicks": true } }1 通あたり最大 100 か所のリンク先が、それぞれ 1 度だけ書き換えられます。ヘッダー画像、ボタン、フッターからリンクされた同じ URL は 1 行にまとまります。同じ問いを 3 回しているだけだからです。上限を超えた残りのリンクは、書かれたとおりに残されます。トラッキングされないリンクでも動作しますし、メッセージが最後の 200 本のリンクを黙って失うことは、不完全なレポートよりはるかに悪い失敗です。
書き換えられたリンクとピクセルは、既定では OpenEmail の API ホストを指します。送信ドメインが tracking.status が active のカスタムトラッキングドメインを持つ場合、そのドメインからの新しいメールは代わりに https://<tracking host>/t/... を使います。設定は PATCH /domains/{id} で行います。
これらすべてに必要なのは emails:read であり、トラッキング専用のスコープはありません。そのスコープはすでに「送信済みメッセージとその配信状況を読む」ことを意味しており、誰かがメッセージを開いたかどうかは、文字どおり最も配信状況に近い情報です。
エンドポイント
| 呼び出し | 返すもの |
|---|---|
| `GET /tracking` | トラッキングされたメッセージを新しい順に。opened、clicked、days(1〜365、既定 30)、limit(最大 200)。 |
| `GET /tracking/stats` | 一定期間の比率。days(既定 30)と offsetMinutes により、読み手の 1 日の区切りに合わせて日が区切られます。 |
| `GET /tracking/{id}` | 1 件のレポート。tmsg_ のトラッキング id、または送信時に返された msg_ の id を受け付けます。 |
| `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 トラフィックについてのレポートになってしまいます。
レポート
{ "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 は取得の回数を数えます。その差(ここでは 4)はスキャナーやプライバシープロキシによるもので、ログと合計値の差が説明不能にならず検証できるように残されています。誰かを名指しする前に読むべきフィールドは attributable です。false は、その閲覧がリスト全員に送られた同一のコピー上で起きたことを意味し、それ以降の特定の受信者についてのあらゆる記述は推測にすぎません。
source は送信元の面を示します。この API 経由の送信は api、アプリ自身が送ったものはすべて composer です。後者では sendId が null になり、だからこそトラッキング id が存在します。
email が null で attributed: false の行は、人物を特定できなかった閲覧が記録される場所です。レポートにこの行が現れるのは、実際にそうした閲覧があったときだけです。受信者が 1 人のメッセージにはこの行がまったくありません。本文が 1 つで宛先が 1 人なら、それは同じ主張だからです。受信者が複数のメッセージは、送出の時点からこの行を背後に持ちます。ディスパッチまでトランスポートが確定しないためです。そして何かが記録されるまでレポートには現れません。名前の付いた受信者の隣に「誰か: 未開封」が常に居座るのは、誤読しかされえない行だからです。この行が存在するとき、名前の付いた行はすべて 0 のままで、attributable は false です。閲覧は実在し、閲覧者はそのメッセージの受信者の誰かであり、「このメッセージの誰か」というのがデータが支持する唯一の表現です。受信者リストから名前を埋めては決していけません。
一定期間の比率
{ "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 }] }比率は送信メール全体ではなく、トラッキングされたメッセージに対する百分率です。10 通に 1 通をトラッキングしているワークスペースの開封率は、その 10 通についてのものであり、送った全メールで割れば、誰かがトラッキングなしの返信を送るたびに下がってしまいます。5 回開封されたメッセージは、開封された 1 通です。比率はメッセージを数え、合計はヒットを数えます。この 2 つを混同するのが、100% を超える開封率が公表される原因です。
byDay は疎です。何もトラッキングされなかった日は 0 ではなく欠落するので、グラフにする前に隙間を埋めてください。日は UTC から東に offsetMinutes(−840 〜 840)の位置で区切られ、読み手の 1 日の区切りに合います。medianTimeToOpenSeconds は平均ではなく中央値です。3 週間遅れて開封された 1 通が、平均値を実際にはどのメッセージも存在しない位置まで引っ張ってしまうからです。
個々のヒット
{ "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 アドレスは保存されません。国・地域・都市はエッジがすでに知っていた情報で、他に保持される識別子は、ソルトが毎日ローテーションされるハッシュだけです。これは 1 日の中では 2 つの取得を区別できますが、翌日には無効になります。
数値が語れないこと
- Apple Mail のプライバシー保護は、誰かが見るかどうかにかかわらず、配信時にすべてのメッセージのすべての画像を取得します。これは User-Agent とネットワークから分類され
machineとして記録されます。送信から 10 秒以内に届いたものも同様です。人の行動がそれほど速く起きることはないからです。 - Gmail の画像プロキシは
machineではなくproxyです。誰かがメッセージを表示したので開封は本物ですが、端末・クライアント・位置は知りえません。プロキシはキャッシュもするため、2 回目の閲覧はこちらに届かないことがあります。Gmail 経由の数値は常に下限であり、総数ではありません。 - 同じコピーに対する 30 秒以内の 2 回の取得は、1 回の閲覧とみなされます。プレビューペインの再描画や、スクロールでメッセージが再表示されたときにも画像は再取得されますが、1 時間後の本物の再訪問はきちんと数えられます。
- 受信者を特定するには、人ごとに組み立て直せるだけ小さいメッセージである必要があります。推定サイズ × 受信者数が 8MB 未満に収まらなければなりません。それを超えると 1 つの本文が全員に送られ、そこへのヒットはすべて特定不能になります。
- クリックがあって開封がないメッセージは、確実に読まれています。画像がブロックされる頻度は、リンクがクリックされない頻度よりはるかに高いからです。2 つのカウンターは足し合わせず、別々に読んでください。
- リンクのない本文についてクリックのトラッキングを要求しても、何も記録されません。送出されるバイト列はトラッキングなしの送信と同一であり、そうでないと主張する行は何とも整合しません。書き換える本文がないメッセージについても同じです。
- OpenEmail は、自社ユーザーが読むメールから 1×1 の画像を(自らが送ったピクセルも含めて)取り除き、画像を表示した状態でメッセージが表示されたときに開封自体を記録します。そのヒットはクライアントが
OpenEmailのhumanになります。画像を非表示にしている場合は何も記録されません。
GET /tracking/{id} と GET /emails/{id}/tracking は、一度もトラッキングされなかったメッセージに対して、空のレポートではなく 404 を返します。「何も記録していません」と「誰も開いていません」は別の答えであり、同じレスポンスを共有してはなりません。一覧エンドポイントはトラッキングされたメッセージだけを保持するため、トラッキングなしのメッセージは 0 が並んだ行として現れるのではなく、単に一覧に存在しません。
尋ねるのではなく通知を受ける
カウントされた開封は email.opened を、カウントされたクリックは email.clicked を、購読中のすべてのエンドポイントに対して発火し、この API を経由したものについては、どちらもそのメッセージ自身のイベント履歴に書き込まれます。スキャナーやプライバシープロキシについてはどちらも発火しません。それらを送れば、分類器が数値から排除するために存在している、まさにそのトラフィックで受信側のログが埋まってしまいます。
ダウンロードリンクとして送られたファイルも同じように報告されます。カウントされたダウンロードは email.downloaded を発火し、同じ履歴に記録されます。同じ分類器がスキャナーやリンクプレビューを排除するため、その数は人の数です。ペイロードはファイルを示す情報(shareId、fileId、filename、mimeType、sizeBytes、url)に加えて、downloadCount、first、downloadedAt と、クリックが持つのと同じクライアント・位置のフィールドを含みます。recipient は常に null、attributed は常に false です。ダウンロードリンクはメッセージの全受信者に対する 1 つの URL なので、ダウンロードを特定の 1 人に結び付けることはできません。
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(),})ここでの呼び出しはすべて単なる読み取りで、クライアントはそれぞれを個別に再試行します。get は OpenEmailApiError を投げ、一度もトラッキングされなかったメッセージでは isNotFound が true になります。これは、どこに取り込む場合でも保っておく価値のある区別です。