تخطَّ إلى المستندات
Python

نقاط النهاية

`webhooks.list` و`list_all` و`iterate` و`get` و`create` و`update` و`delete` و`rotate_secret` و`test` و`get_delivery` و`replay_delivery`، وسجلّا التسليم والنشاط.

كل الدوالّ

usage.py
from acme.secrets import store endpoint = client.webhooks.create({    'url': 'https://acme.com/hooks/mail',    'eventTypes': ['email.sent', 'email.bounced'],    'description': 'Billing service',}) store(endpoint['secret']) client.webhooks.list()client.webhooks.get(endpoint['id'])client.webhooks.update(endpoint['id'], {'enabled': False})client.webhooks.test(endpoint['id'])latest = client.webhooks.list_deliveries(endpoint['id'], limit=1)['items'][0]client.webhooks.get_delivery(endpoint['id'], latest['id'])client.webhooks.replay_delivery(endpoint['id'], latest['id'])rotated = client.webhooks.rotate_secret(endpoint['id'])store(rotated['secret'])client.webhooks.delete(endpoint['id'])

نداء create هو المرة الوحيدة التي يُعاد فيها السر، عدا rotate_secret. ولا تعيده القراءة أبدًا، فخزّنه قبل أي شيء آخر. وأغفل eventTypes لتحصل على المجموعة الافتراضية، أي كل حدث email.* عدا email.replied. ولا تصل email.replied وdomain.* وsuppression.* وfile.* وform.* إلى نقطة نهاية إلا إذا سمّتها.

ليس لـrotate_secret نافذة تداخل. فالسر القديم يتوقف عن العمل فورًا، فانشر الجديد قبل التدوير. ولا تُعاد محاولته تلقائيًا أبدًا: فإعادة المحاولة ستدوّر مرة ثانية وتبطل السر الذي أعادته المحاولة الأولى.

ما يمكنك الاشتراك فيه

يُصدَّر WEBHOOK_EVENTS كي تتمكن من عرض القائمة. والأحداث أحداث **صندوق البريد** لا أحداث هذا API: فـemail.received ينطلق للبريد الذي يصل إلى التطبيق، وemail.sent ينطلق لرسالة أرسلها المحرِّر. والاشتراك ليس كمراقبة حركة API الخاصة بك.

ينطلق file.uploaded عندما يوضع ملف على صفحة الملفات، وfile.deleted عندما يُحذف ملف. وبياناتهما FileEventData: fileId وfilename وmimeType وsizeBytes وdirection وto وthreadId وmessageId، وuploadedAt أو deletedAt. وto هو العنوان الذي ينتمي إليه الملف، أو null لملف ينتمي إلى مساحة العمل كلها.

أحداث الملفات ليست في المجموعة الافتراضية، فلا تستقبلها نقطة النهاية إلا إذا سمّتها في eventTypes. ونقطة النهاية المقيّدة ببعض العناوين لا تسمع إلا عن ملفات تلك العناوين، فالرفع الخاص بمساحة العمل كلها، مع to بقيمة null، لا يُرسل إليها.

ينطلق form.submitted عندما يشترك شخص عبر أحد نماذجك، وform.confirmed عندما ينضم اشتراك معلّق إلى الجماهير، لأن الشخص فتح رابط التأكيد أو لأنك وافقت عليه. ويحمل form.submitted البيانات FormSubmittedEventData: formId وformName وsubmissionId وemail وstatus وanswers وaudienceIds وsourceUrl وsubmittedAt. ويحمل form.confirmed البيانات FormConfirmedEventData: formId وformName وsubmissionId وemail وaudienceIds وvia، وقيمته link أو approval، وconfirmedAt.

الاشتراك في نموذج بلا تأكيد مزدوج يرسل form.submitted مع status بقيمة added ولا يرسل form.confirmed، لذا عامل هذا الزوج على أنه لحظة انضمام الشخص. ومن يشترك مرة أخرى قبل التأكيد يحتفظ بنفس submissionId، ولا يُرسل form.submitted مجددًا إلا إذا تغيّرت إجاباته. وأحداث النماذج ليست في المجموعة الافتراضية، ونقطة النهاية المقيّدة ببعض العناوين لا تتلقاها أبدًا، لأن الاشتراكات تخص مساحة العمل كلها.

كل شكل من أشكال البيانات هذه هو TypedDict في openemail.types. أضِف إلى حدث تم التحقق منه تلميح النوع WebhookPayload[FileEventData]، مثلًا، فيعرف مدقق الأنواع ما يحمله event['data'].

إثبات أنه يعمل

webhook_test.py
result = client.webhooks.test('whe_…')delivery = result['delivery'] if delivery is not None:    print(delivery['status'], delivery['responseCode']) for d in client.webhooks.iterate_deliveries('whe_…'):    print(d['eventType'], d['status'], d['responseCode'], d['error'])

قيمة responseCode التي هي None تعني أنه لم تكن هناك استجابة إطلاقًا (DNS أو TLS أو انتهاء مهلة)، وهي حقيقة مختلفة عن استجابة قالت 0. ويحمل كل صف attempt وmaxAttempts، فقد تصف عدة صفوف حدثًا واحدًا: فالقيمة eventId نفسها عبرها هي الحدث، ورقم المحاولة هو المحاولة. وتقول nextAttemptAt متى يحين موعد إعادة المحاولة التلقائية التي تلي الصف.

إرساله مرة أخرى

webhook_replay.py
detail = client.webhooks.get_delivery('whe_…', 'whd_…')print(detail['payload'], detail['responseBody'], detail['replayRefusal']) replay = client.webhooks.replay_delivery('whe_…', 'whd_…')print(replay['delivery']['status'], replay['delivery']['responseCode'])

التسليم الذي يستمر في الفشل يُجرَّب حتى 8 مرات: فور حدوثه، ثم بعد دقيقة واحدة، و5 دقائق، و30 دقيقة، وساعتين، و5 ساعات، و10 ساعات، و10 ساعات أخرى، أي نحو 27 ساعة ونصف في المجموع. ولا يُكرَّر إلا الفشل الذي يستحق التكرار: لا استجابة، أو 408 أو 425 أو 429 أو 5xx. وإعادة التشغيل ترسل الحدث المخزَّن مرة أخرى بالقيم نفسها لـid وtype وcreatedAt وdata، فالمستقبِل الذي يُسقط المعرّفات التي عالجها من قبل يعامله على أنه الحدث الذي يعرفه. الجديد هو التوقيع وحده.

  • ترسل replay_delivery حدثًا واحدًا الآن وتعيد ما أجاب به خادمك. وتعمل على محاولة سُلِّمت أيضًا، ولا تُعاد محاولتها أبدًا. وقبل أن ترسل، تُوقَف مؤقتًا إعادات المحاولة التلقائية لذلك الحدث التي لم تبدأ بعد: تبقى ملغاة إذا سُلِّمت إعادة الإرسال، وتُستأنف في موعدها إذا فشلت.
  • إذا كانت إعادة محاولة تلقائية للحدث نفسه تُرسَل في تلك اللحظة، فلا ترسل replay_delivery شيئًا وتُرفض بـ 409 retry_in_progress، وما دامت إعادة تشغيل أخرى له قيد الإرسال تُرفض بـ 409 replay_in_progress، فلا يتلقى المستقبِل نسختين في الوقت نفسه أبدًا، حتى من إعادتي تشغيل أُرسلتا في اللحظة نفسها. انتظر بضع ثوانٍ واقرأ get_delivery، فقد تُسلِّم تلك المحاولة أو إعادة التشغيل الحدث. وإعادة التشغيل حدث واحد في كل مرة: لا يوجد نداء يعيد إرسال كل تسليم فاشل.
  • وترفض أيضًا بـ 409 نقطة نهاية مُطفأة (webhook_disabled)، وحدثًا لم تعد نقطة النهاية تستمع إليه (event_not_subscribed) أو لم تعد تغطيه (event_out_of_scope)، ومحاولة بلا حدث مخزَّن (delivery_not_replayable). وتُبلغ get_delivery بهذه الإجابة مسبقًا في replayRefusal.

لا يعيد SDK محاولة replay_delivery من تلقاء نفسه أبدًا، لأن إعادة المحاولة بعد استجابة ضائعة سترسل الحدث مرة أخرى.

يرفع كل رفض OpenEmailApiError مع status بقيمة 409، وis_conflict بقيمة true، والسبب في code، وهو إحدى القيم في WEBHOOK_REPLAY_ERROR_CODES.

المعاملات: webhooks.create

urlstrمطلوب
الوجهة التي تُرسَل إليها التسليمات بـPOST. وبـHTTPS حصرًا، ولا يجوز أن يكون المضيف `localhost` ولا اسمًا من نوع `.localhost`/`.local`/`.internal`، ولا عنوان IP حرفيًا من نوع loopback أو خاص أو CGNAT أو link-local. فهذا جلب من جهة الخادم إلى عنوان تزوّده أنت، ولذلك تعطي تلك الحالات 422 على `url`؛ ويقرأ الفحص اسم المضيف كما كُتب ولا يحلّ DNS أبدًا. والمخزَّن هو ما يسلسله محلّل URL لما أرسلته، فـ`https://acme.com` يُقرأ مجددًا `https://acme.com/`.
eventTypeslist[WebhookEvent]
الأحداث التي تصل إلى نقطة النهاية هذه: أي من الأسماء في `WEBHOOK_EVENTS`. ويحدّ `POST /webhooks` المصفوفة بعدد الأحداث الموجودة، فواحد فوق ذلك يعطي 422 على `eventTypes`؛ أما `PATCH` فلا يحدّها. والمحدود هو الطول فقط، والاسم المكرر يُخزَّن ويُقرأ تمامًا كما أرسلته. والإغفال أو الفراغ يُخزَّن كقائمة فارغة، ولهذا يُقرأ مجددًا `['*']`، وهو يعني كل حدث `email.*` عدا `email.replied`، أي أربعة عشر حدثًا اليوم، ولا يعني أبدًا عائلات النطاقات أو الكبح أو الملفات. والعائلة المضافة لاحقًا لا تصل أبدًا إلى نقطة نهاية لم تسمّها، فلا يمكن لتكامل أن يبدأ باستقبال شكل لم يره قط بسبب إصدار جديد.
descriptionstr
تسمية لنقطة النهاية، بحد أقصى 200 حرف، كي تُقرأ قائمة الـwebhooks كأسماء لا كعمود من عناوين URL. وإن أُغفلت، خُزّنت وأُعيدت كـnull.

الاستجابة: CreatedWebhookResource

objectLiteral['webhook']
دائمًا `'webhook'`، وهو المميِّز نفسه الذي تعيده القراءة العادية، لأن السر مفتاح إضافي واحد على الشكل العادي لا نوع كائن مستقل. ووجود `secret` من عدمه يحدّده التابع الذي ناديته، لا هذا الحقل.
idstr
معرّف نقطة النهاية: `whe_` متبوعًا بـ24 حرفًا ست عشريًا. ويأخذه كل نداء webhook آخر: `get` و`update` و`delete` و`rotate_secret` و`test` و`list_deliveries` و`list_all_deliveries` و`iterate_deliveries` و`get_delivery` و`replay_delivery`.
urlstr
نقطة النهاية كما خُزّنت، بعد اجتياز فحص HTTPS وفحص المضيفات المحظورة. وهي عنوان URL المحلَّل بعد إعادة تسلسله، فقارن بهذه القيمة لا بالسلسلة التي أرسلتها.
descriptionstr | None
التسمية التي أعطيتها لها، أو null إن لم تعطِ شيئًا. وطلب `update` الذي يرسل null صريحة يمسحها ويعيدها إلى null.
eventTypeslist[WebhookEvent] | ['*']
الأحداث المشترَك فيها، أو `['*']` حين لم تسمِّ نقطة النهاية أي حدث. والقيمة `['*']` هي طريقة عرض قائمة مخزَّنة فارغة عند القراءة ولا يمكن إرسالها مجددًا، وهي تمثّل أحداث الرسائل الأربعة عشر لا الفهرس كله. ولا يقبل `create` و`update` إلا أسماء الأحداث الحرفية.
enabledbool
ما إذا كانت التسليمات تُحاوَل؛ فنقطة النهاية المعطّلة تُتخطى عند إرسال الأحداث وتحتفظ بسرّها وبسجل تسليماتها. وهي true دائمًا هنا، لأن `WebhookCreate` لا يحتوي `enabled` و`WebhookPatch` وحده يحتويه.
lastDeliveryAtstr | None
طابع وقت بصيغة ISO 8601 لآخر محاولة تسليم، لا لآخر نجاح. ويُختم بعد طلب POST فاشل أيضًا، فهو يخبرك بأن نقطة النهاية جُرّبت، و`list_deliveries` يخبرك كيف سارت المحاولة. ويكون null حتى المحاولة الأولى، ولذلك هو null دائمًا في `create`.
createdAtstr
طابع وقت بصيغة ISO 8601 لوقت تسجيل نقطة النهاية. ويعيد `list` نقاط النهاية من الأحدث إلى الأقدم بحسب هذا الحقل.
secretstr
مفتاح HMAC-SHA-256 الذي يوقّع ترويسة `X-OpenEmail-Signature` في كل تسليم: `whsec_` متبوعًا بـ32 بايتًا عشوائيًا بترميز base64url، وهو ما تسلّمه إلى `verify_webhook_signature`. ويعيده `create` و`rotate_secret` ولا شيء غيرهما. ولا تعيده القراءة أبدًا، فخزّنه الآن؛ والسر الضائع لا يمكن استبداله إلا بـ`rotate_secret`، الذي يبطل القديم فورًا.

ترشيح السجلات

webhook_logs.py
from datetime import datetime, timedelta, timezone failed = client.webhooks.list_workspace_deliveries(    status='failed',    since=datetime.now(timezone.utc) - timedelta(days=1),)print(len(failed['items'])) history = client.webhooks.list_activity('whe_…')print([(change['type'], change['actor']['label'] if change['actor'] else None) for change in history['items']])

يقرأ list_deliveries نقطة نهاية واحدة، ويقرأ list_workspace_deliveries كل نقاط النهاية أو تلك التي يسمّيها endpoint_ids=، وكلاهما يقبل status= وsince= وuntil=، وهي مرشّحات تبويب التسليمات في وحدة التحكم. ويقرأ list_activity وlist_workspace_activity سجل التدقيق: من أنشأ ماذا أو غيّره أو أوقفه أو شغّله أو دوّره أو اختبره أو أعاد إرساله أو أزاله. لكل منها list_all_… وiterate_… بجانبه، ويحمل كل صف في سجل مساحة العمل endpointId.

يقبل since= وuntil= قيمة datetime أو سلسلة ISO 8601. ويُقرأ datetime الساذج (naive) بالتوقيت المحلي ثم يُحوَّل إلى UTC، فمرّر قيمة مدركة للمنطقة الزمنية (aware)، كما في المثال أعلاه.

المرجع