قاعدة المعرفة
Webhooks
تُخبر نقطة النهاية لديك عند وصول البريد، بدل أن تضطرك إلى الاستقصاء المتكرر.
التفاصيل
- قابلة للاستخدام اليوم من Settings → Webhooks وعبر API: سجّل نقطة نهاية https، واختر أيًّا من الأحداث العشرين تريدها، وانسخ سر التوقيع whsec_ الذي يُعرض عند الإنشاء وعند التدوير ولا يُعرض بعد ذلك أبدًا. والتسليمات طلبات POST موقّعة حقيقية يطلقها صندوق البريد نفسه لا أي استدعاء لـ API، فتنطلق عند وصول البريد وعند عمليات الفتح والنقر أيًّا كان ما أرسل الرسالة. ويطلق الإرسال من كل واجهة، وكان يطلق من بعضها فقط: فالإرسال عبر API أو MCP أو قالب أو قاعدة كان يطلق email.sent بينما لا يفعل ذلك ما يُرسل من نافذة الإنشاء في التطبيق، لأن نافذة الإنشاء تكتب إلى صندوق البريد مباشرة لا عبر خدمة الإرسال التي كانت تصدر الحدث. أما الآن فيُطلق الحدث عند صندوق البريد نفسه، وهو الموضع الذي تلتقي فيه كلها، فصار الإنشاء في التطبيق والجدولة ليوم الثلاثاء والإرسال إلى API ثلاث طرق لإحداث الـ webhook نفسه. والإرسال المؤجّل يعلن عن نفسه مرتين: email.scheduled أو email.queued عند قبوله، وemail.sent عند خروجه فعليًا، وemail.cancelled إن تراجعت عنه بينهما. عشر نقاط نهاية لكل صندوق بريد، تُطبَّق حيثما سُجّلت واحدة لا على هذه الشاشة وحدها.
- تأتي الأحداث في ثلاث عائلات. خمسة عشر منها يخص رسالة واحدة: email.received وemail.replied وemail.sent وemail.delivered وemail.failed وemail.cancelled وemail.scheduled وemail.queued (شقيق scheduled الخاص بالتراجع عن الإرسال) وemail.delivery_delayed وemail.bounced وemail.complained وemail.suppressed وemail.opened وemail.clicked وemail.downloaded. ويعني email.sent أن خدمة الإرسال قبلت الرسالة، ويعني email.delivered أن الخادم المستقبِل قبلها، ويعني email.delivery_delayed أنها لم تصل بعد ولا تزال قيد إعادة المحاولة. وينطلق email.replied إلى جانب email.received عندما تكون الرسالة الواصلة ردًّا على رسالة موجودة في صندوق البريد، فالمستهلك الذي يريد الاثنين يحصل عليهما. وينطلق email.downloaded عندما يجلب شخص ملفًا خرج على هيئة رابط تنزيل، مع المصنّف نفسه الذي يبقي الماسحات ومعاينات الروابط خارج العدّ، وهو لا يسمّي أي مستلم، لأن الرابط واحد لكل من أُرسلت إليهم الرسالة. وثلاثة أحداث تخص نطاقًا: domain.verified عندما يبدأ الاستقبال، وdomain.sending_changed عندما يتغيّر حكم الإرسال الخاص به، وdomain.deleted عند إزالته، سواء طلبت ذلك أم أسقطه حاصد الأيام السبعة وهو غير موثّق. وحدثان يخصان قائمة الحجب نفسها، وهي شيء مختلف عن email.suppressed: suppression.added عندما يُضاف عنوان، وsuppression.removed عندما يُسمح بعنوان من جديد. والاشتراك في لا شيء منها يعني كل أحداث الرسائل عدا email.replied، أي أربعة عشر اليوم، ولا تُضاف إليها عائلة لاحقًا أبدًا، وتعيدها API قراءةً بوصفها ["*"]. سمِّ الأحداث التي تريدها إن كنت تفضّل التصريح. وتحمل كل عملية تسليم X-OpenEmail-Signature بصيغة t=<unix>,v1=<hex>، وهو HMAC-SHA-256 على الطابع الزمني ونقطة والجسم الخام، إضافة إلى X-OpenEmail-Event وX-OpenEmail-Delivery. تحقّق مقابل البايتات كما وصلت: فالتحليل وإعادة التسلسل يعيدان ترتيب المفاتيح ويكسران التوقيع. ونافذة إعادة التشغيل البالغة 300 ثانية مسؤولية المستقبِل، ومُتحقِّق SDK يعتمدها افتراضيًا.
- يُرفض التسجيل لأي شيء ليس https أو غير قابل للتوجيه علنًا (loopback وRFC1918 وlink-local وCGNAT وما يقابلها في IPv6)، ولا تُتبَع عمليات إعادة التوجيه، فيُسجَّل رد 3xx تسليمًا فاشلًا بدل ملاحقته إلى مكان آخر. ويحصل المستقبِل على 5 ثوانٍ، وتُسلَّم نقاط النهاية بالتوازي فتكلّف عشر منها 5 ثوانٍ لا 50، وتُدرج كل المحاولات، صفحةً بعد صفحة، في صفحة تلك النقطة مع رمز الاستجابة والمدة التي استغرقتها.
- تُحاوَل عملية التسليم حتى 8 مرات. تخرج الأولى لحظة وقوع الحدث؛ والفشل الذي يُحتمل أن يزول من تلقاء نفسه يُعاد بعد دقيقة واحدة، ثم 5، ثم 30، ثم ساعتين، و5 ساعات، و10 ساعات، و10 ساعات أخرى، ما يوزّع حدثًا واحدًا على نحو 27 ساعة ونصف. ويتفاوت كل انتظار بما يصل إلى العُشر، فلا تعود ألف حدث فشلت معًا في الثانية نفسها، وتُحترم ترويسة Retry-After من نقطة النهاية إذا طلبت انتظارًا أطول، حتى 6 ساعات. وتُحفظ إعادات المحاولة عملًا دائمًا لا في الذاكرة، فلا يضيّعها نشر يقع في منتصف تلك النافذة. ولا يُعاد إلا الفشل الذي يستحق التكرار: انتهاء مهلة، أو اتصال مرفوض، أو 408 أو 425 أو 429 أو أي 5xx. وأي 4xx آخر هو رفض متعمَّد من نقطة النهاية للحمولة، والسؤال سبع مرات إضافية سيكون سبعة أضعاف الحِمل للجواب نفسه. ويُثبَّت معرّف الحدث وcreatedAt الخاص به مرة واحدة وتحملهما كل محاولة، والمعرّف في X-OpenEmail-Delivery أيضًا، فالمستقبِل الذي يرى المعرّف نفسه مرتين يستطيع إسقاط الثانية بدل التصرف بناءً عليها مرتين. وبعد إصلاح نقطة النهاية، يمكن إعادة تشغيل التسليم الذي فشل من سجل التسليم في التطبيق أو عبر API، حدثًا واحدًا في كل مرة، وتحمل إعادة التشغيل المعرّف نفسه. وتوقف إعادة التشغيل مؤقتًا إعادات المحاولة التلقائية لذلك الحدث أثناء إرسالها، وتُرفض إذا كانت إحداها تُرسَل في تلك اللحظة، فلا يتلقى المستقبِل نسختين في الوقت نفسه أبدًا. وبعد فشل 100 حدث متتالٍ في كل محاولاتها، تُعطَّل نقطة النهاية، وتُراسَل مساحة العمل بالبريد، ويكون السبب مقروءًا على نقطة النهاية نفسها. أما نقطة النهاية التي تجيب بـ 410 Gone فتُعطَّل في الحال.
- نقطة النهاية التي تفشل 100 مرة متتالية تُطفأ بدل الاتصال بها إلى الأبد، ويُراسَل بالبريد كل من لديه وصول إلى webhooks لإبلاغه: أي نقطة، وماذا أفادت آخر محاولة، وأن شيئًا لم يُوضع في الطابور أثناء فشلها. والعدّ متتالٍ، وأي محاولة مُسلَّمة تصفّره، فلا يمكن لظهيرة سيئة في مارس الماضي أن تتراكم حتى تعطّل نقطة نهاية اليوم. وإعادة تشغيلها تمسح العدّ معها. وتميّز وحدة التحكم بين الحالتين بدل عرض مفتاح واحد: فنقطة أوقفتها أنت تبدو مختلفة عن نقطة أوقفناها نحن.
- إدارة نقاط النهاية مهمة واحدة لها بابان. عبر API هي POST /webhooks والتعديل والحذف وتدوير السر والاختبار وسجل التسليم وإعادة التشغيل، ولكل منها طريقة في SDK؛ وفي التطبيق هي Settings → Webhooks، مقابل السجل نفسه لا سجل ثانٍ. والقراءة محكومة بـ webhooks:read، فيستطيع أي شخص يبني تكاملًا أن يرى نقاط النهاية وتاريخ تسليمها (أيها انطلق، وبماذا أجاب المستقبِل، وكم استغرق) من دون أن يكون المالك. أما التسجيل والتحرير والاختبار والتدوير وإعادة التشغيل والحذف فتحتاج إلى webhooks:write وإلى ملكية صندوق البريد معًا، على الواجهتين، وهذا الشطر الثاني متعمَّد: فنقطة النهاية تسمع عن كل عنوان تملكه مساحة العمل ما لم تضيّقها قوائم السماح الخاصة بها، بموضوعاته ومستلميه، ولا يوجد إذن يعني «يجوز إرسال كل ذلك إليه». والدور الذي يبني التكاملات ولا يقرأ البريد يشغّلها بمفتاح مساحة عمل بدلًا من ذلك. وفتح عملية تسليم لقراءة المتن المرسَل والرد كاملًا يتطلب الملكية أيضًا، لأن ذلك المتن يحمل العناوين والمستلمين أنفسهم.
- تُقرأ السجلات بالطريقة نفسها في كل مكان. يقرأ GET /webhooks/deliveries سجل التسليم لكل نقاط النهاية دفعة واحدة، ويقرأ GET /webhooks/{id}/deliveries سجل نقطة واحدة، وكلاهما مقيّد بالحالة، أي مفتاح «الفاشلة فقط»، وبفترة زمنية، ويقرأ GET /webhooks/activity وGET /webhooks/{id}/activity من أنشأ ماذا أو غيّره أو أوقفه أو شغّله أو دوّره أو اختبره أو أعاد إرساله أو أزاله، بصيغة @username أو باسم مفتاح API الذي فعل ذلك. لدى SDK تابع لكل منها، ولدى خادم MCP الأدوات listWebhookDeliveries وgetWebhookDelivery وlistWebhookActivity وreplayWebhookDelivery الذي يسأل دائمًا قبل الإرسال. تبقى قراءة محتوى التسليم للمالك وحده على كل واجهة، ولا يستطيع مفتاح مقيّد بعنوان واحد في نطاق ما قراءة تسليمات نقطة نهاية تغطي النطاق كله.