تتبع الفتح والنقر
GET /tracking: هل قُرئت رسالة، وما الروابط التي فُتحت.
ينفّذ أيًّا من الاستدعاءات الـ6 في هذه الصفحة على مساحة عملك، بمفتاحك أنت.
ما الذي يُسجَّل
مفتاحان مستقلان، وكلاهما مفعّل ما لم يُطفأ للعنوان المرسَلة منه الرسالة أو لكل العناوين. فـ opens يُلحق صورة بحجم 1×1؛ وclicks يعيد كتابة الروابط في الجزء الجديد من النص. أما التاريخ المقتبس أسفل الرد فهو رسالة شخص آخر ويُترك كما هو. ويسمّي الإرسال tracking: { opens, clicks } ليقرّر لرسالة واحدة (في أي اتجاه، فـ false هي طريقة برنامج في رفض ما ضُبط العنوان على فعله)، والحقل الذي تحذفه يعود إلى إعداد العنوان المرسَلة منه، ثم إلى كل العناوين، لا إلى قيمة افتراضية اختارتها هذه الـ API نيابة عن مساحة عمل.
{ "from": "Acme Billing <[email protected]>", "to": ["[email protected]"], "subject": "Your September invoice", "html": "<p>Invoice attached.</p>", "tracking": { "opens": true, "clicks": true } }يُعاد كتابة 100 وجهة على الأكثر لكل رسالة، مرة واحدة لكل وجهة. والرابط نفسه الموضوع في صورة ترويسة وزر وتذييل هو صف واحد، لأنه سؤال واحد طُرح ثلاث مرات. وبعد الحد الأقصى تُترك الروابط الباقية تمامًا كما كُتبت: فالرابط غير المتتبَّع يظل يعمل، والرسالة التي تفقد بصمت آخر مئتي رابط فيها إخفاق أسوأ بكثير من تقرير ناقص.
تشير الروابط المعاد كتابتها والبكسل إلى مضيف 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، فتنقسم الأيام حيث ينقسم يوم القارئ. |
| `GET /tracking/{id}` | تقرير واحد. يأخذ معرّف تتبع tmsg_ أو المعرّف msg_ الذي أعاده الإرسال. |
| `GET /tracking/{id}/opens` | عمليات الجلب المفردة. includeMachine وlimit (بحد أقصى 200). |
| `GET /tracking/{id}/clicks` | الشيء نفسه، مع linkId وurl في كل صف. |
| `GET /emails/{id}/tracking` | التقرير نفسه، انطلاقًا من معرّف الإرسال الذي تحمله بالفعل. |
القيم المنطقية تُكتب صراحة في سلسلة الاستعلام: 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 يعدّ عمليات الجلب. والفرق، وهو أربعة هنا، هو الماسحات ووسطاء الخصوصية، وقد حُفظ ليبقى الفارق بين السجل والمجموع قابلًا للفحص لا غامضًا. وattributable هو الحقل الذي تقرؤه قبل تسمية أي شخص: فالقيمة false تعني أن قراءة وقعت على نسخة ذهبت إلى القائمة كلها، وكل جملة عن مستلِم بعينه بعد ذلك تخمين.
source يسمّي السطح الذي أرسلها: api للإرسال عبر هذه الـ API، وcomposer لكل ما أرسله التطبيق نفسه. وsendId يكون null في النوع الثاني، ولهذا يوجد معرّف التتبع.
الصف الذي فيه email فارغ وattributed: false هو موضع القراءة التي تعذّر ربطها بشخص، ولا يعرضه التقرير إلا حين تقع قراءة فعلًا. والرسالة ذات المستلِم الواحد لا تحمل منه شيئًا، لأن نصًا واحدًا ومرسَلًا إليه واحدًا هما التصريح نفسه. أما الرسالة ذات المستلِمين المتعددين فلها صف من هذا النوع خلفها منذ لحظة خروجها، لأن النقل لا يستقر حتى الإرسال، ويبقى خارج التقرير حتى يصل عليه شيء: فسطر دائم يقول "شخص ما: لم يفتح" بجوار المستلِمين المسمّين صفٌّ لا يمكن إلا أن يُساء فهمه. وحيث يكون موجودًا تكون الصفوف المسمّاة هي الجالسة عند الصفر وتكون 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 }] }المعدلات نسب مئوية على الرسائل المتتبَّعة لا على كل البريد المرسَل: فمساحة العمل التي تتبع رسالة من كل عشر لها معدل فتح لتلك العشر، وقسمتها على كل ما أرسلته يومًا ستهبط كلما أرسل أحدهم ردًا غير متتبَّع. والرسالة التي فُتحت خمس مرات هي رسالة مفتوحة واحدة. فالمعدلات تعدّ الرسائل والمجاميع تعدّ الإصابات، والخلط بينهما هو ما يجعل معدلات فتح تتجاوز 100% تُنشر.
byDay متفرّق: فاليوم الذي لم يُتتبَّع فيه شيء غائب لا مساوٍ للصفر، فاملأ الفجوات قبل رسمه بيانيًا. وتُجمَّع الأيام عند offsetMinutes شرق UTC (من −840 إلى 840) لتنقسم حيث ينقسم يوم القارئ. وmedianTimeToOpenSeconds وسيط لا متوسط، لأن رسالة واحدة فُتحت بعد ثلاثة أسابيع تجرّ المتوسط إلى موضع لا توجد فيه أي رسالة.
الإصابات المفردة
{ "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 لأي إصابة. والدولة والمنطقة والمدينة هي ما كانت الحافة تعرفه أصلًا، والمعرّف الآخر الوحيد المحفوظ هو تجزئة يدور ملحها يوميًا، فتستطيع التمييز بين عمليتي جلب داخل اليوم الواحد وتصير بلا أثر في اليوم التالي.
ما لا تستطيع الأرقام قوله
- تجلب حماية خصوصية Apple Mail كل صورة في كل رسالة عند التسليم سواء نظر أحد أم لا. وتُصنَّف من 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
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 فيه صحيحة لرسالة لم تُتتبَّع قط، وهذا هو التمييز الجدير بالحفاظ عليه أيًّا كان ما تغذّيه به.