Webhooks
اضبط عنوان webhook لتُعيد Sendveo توجيه كل رسالة واردة حقيقية إليه في الوقت الفعلي، لتتفاعل أنظمتك دون استعلام دوري.
ضبط الـ webhook
اضبط نقطة النهاية الخاصة بك أو استبدلها. تتضمّن الاستجابة سرًا مشتركًا secret، يُعرض عند إنشاء الـ webhook أو عند تدويره. يجب أن يكون العنوان قابلًا للوصول علنًا عبر HTTPS.
curl -X PUT https://api.sendveo.com/v1/webhook \
-H "X-API-KEY: sv_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "url": "https://hooks.yourapp.com/sendveo/inbound" }'{
"webhook": {
"url": "https://hooks.yourapp.com/sendveo/inbound",
"active": true,
"secret": "whsec_2f8b41c9a7e04d6fb0c3e1a95d72"
}
}حمولة إعادة التوجيه
تُسلَّم كل رسالة واردة كطلب POST بصيغة JSON إلى عنوانك:
{
"type": "message.received",
"message": {
"id": "msg_7Qk2",
"channelId": "chn_sales01",
"chatId": "chat_aa11",
"from": "+212670111222",
"senderName": "Khadija Alaoui",
"text": "Hi, is the order still available?",
"timestamp": "2026-09-09T14:02:11Z"
}
}ردّ على رسالة بالإرسال إلى chatId الخاص بها: POST /v1/messages مع { channelId, to: chatId, text }.
فهرس الأحداث
تُرسَل ستة عشر نوعًا من الأحداث بطلب POST إلى عنوانك، وكلها تحمل السر المشترك في ترويسة x-sendveo-secret. ويحمل كل حدث occurredAt (وقت وقوعه، لا وقت إعادة توجيهنا له). أحداث الرسائل الثمانية تحمل الرسالة كاملة تحت message، بالشكل نفسه الذي يستعمله message.received، فيقرأها محلّل واحد جميعًا؛ وأحداث المجموعات الثلاثة تتعلّق بمحادثة وتحمل chat بدلًا من ذلك؛ وأحداث الأرقام الخمسة تتعلّق بخط وتحمل number.
| الحدث | يُطلَق متى | حقول إضافية |
|---|---|---|
| message.received | ترسل جهة اتصال رسالة. | لا شيء |
| message.sent | أرسل رقمك رسالة، من الهاتف نفسه أو عبر Sendveo. | message.origin |
| message.delivered | وصلت رسالة صادرة إلى جهاز المستلِم. | لا شيء |
| message.read | قُرئت رسالة صادرة. | لا شيء |
| message.failed | رُفضت رسالة صادرة. | reason |
| message.reaction | تفاعل أحدهم، أو سحب تفاعله. | emoji, by |
| message.edited | عدّل المُرسِل الرسالة. | by |
| message.deleted | حُذفت الرسالة لدى الجميع. | by |
| group.member_joined | أُضيف شخص إلى مجموعة. | chat, member, by |
| group.member_left | غادر شخص مجموعة، أو أُزيل منها. | chat, member, by |
| group.updated | أُنشئت مجموعة، أو تغيّر موضوعها. | chat, subject, by |
| number.disconnected | توقّف الرابط مع الهاتف عن العمل | number, reason |
| number.reconnected | عاد الرابط إلى العمل | number |
| number.phone_offline | الرابط سليم لكن الهاتف لا يستجيب | number |
| number.phone_online | عاد الهاتف يستجيب | number |
| number.history_synced | تم استيراد سجل المحادثات الأخير لرقم جرى ربطه حديثًا | number, counts |
{
"type": "message.read",
"occurredAt": "2026-09-16T11:01:00Z",
"by": "+212670111222",
"message": {
"id": "msg_8Rm5",
"channelId": "chn_sales01",
"chatId": "chat_aa11",
"from": null,
"senderName": null,
"text": "Votre commande est prete.",
"timestamp": "2026-09-16T10:59:00Z",
"type": "text",
"status": "READ",
"attachments": [],
"replyTo": null,
"location": null,
"contacts": null,
"linkPreview": null
}
}{
"type": "message.reaction",
"occurredAt": "2026-09-16T11:03:00Z",
"emoji": "❤️",
"by": "+212670111222",
"message": { "id": "msg_8Rm5", "chatId": "chat_aa11", "type": "text", "status": "READ" }
}سحب التفاعل هو الحدث message.reaction نفسه مع "emoji": null، لا نوعًا ثامنًا، فمعالِج واحد يغطّي الحالتين.
لا يُعاد توجيه التسليمات المكرّرة مرتين: فخدمة الرسائل تعيد تسليم الأحداث كما تعيد تسليم الرسائل، والتكرار يُحذف فيُطلَق الـ webhook مرة واحدة. أما الحدث المتعلّق برسالة لا تحتفظ بها Sendveo (محادثة أقدم من تاريخ الربط) فيُستلَم ثم يُهمَل: لا يُعاد توجيهه، وليس خطأ.
أحداث المجموعات
تحمل أحداث المجموعات الثلاثة chat حيث تحمل أحداث الرسائل message، إضافة إلى by (من قام بالإجراء). وchat.members هي قائمة الأعضاء المختصرة: من هم، وما أدوارهم، وهل الرقم رقمك أنت.
{
"type": "group.member_joined",
"occurredAt": "2026-09-16T11:40:00Z",
"by": "+212661234567",
"member": { "phone": "+212612345678", "name": "Hind Zerouali", "removed": false },
"chat": {
"id": "cht_m8k3x7q2a1b4",
"chatId": "120363001122334455@g.us",
"channelId": "chn_sales01",
"kind": "group",
"name": "Atlas team - deliveries",
"members": [
{ "phone": "+212661234567", "name": null, "role": "super_admin", "self": true },
{ "phone": "+212612345678", "name": "Hind Zerouali", "role": "member", "self": false }
]
}
}{
"type": "group.updated",
"occurredAt": "2026-09-16T11:45:00Z",
"by": "+212661234567",
"subject": "Atlas team - deliveries 2026",
"chat": { "id": "cht_m8k3x7q2a1b4", "chatId": "120363001122334455@g.us", "kind": "group" }
}الإزالة والمغادرة حدث واحد، هو group.member_left، ويفرّق بينهما member.removed، وهو الاختيار نفسه الذي يتّبعه message.reaction للتفاعل وإلغائه. وإنشاء المجموعة وتغيير اسمها حدث واحد أيضًا، هو group.updated، لأن كليهما يعني للمعالِج «تغيّرت تفاصيل هذه المجموعة»؛ وsubject هو الموضوع بعد التغيير.
تصل الرسالة المُرسَلة في مجموعة بوصفها message.received عاديًا يحمل message.group (أي محادثة) وmessage.author (من قالها)، ولا يوجد حدث منفصل لرسائل المجموعات. ويكون الحقلان null في الرسالة الثنائية، وهو تمامًا ما يظل يراه معالِج كُتب قبل وجود المجموعات.
الحدث المتعلّق بمحادثة على رقم لا تعرفه Sendveo يُستلَم ثم يُهمَل، شأن كل حدث عن حساب غير معروف. فلا يُعاد توجيهه، وليس خطأ.
أحداث الأرقام
خمسة أحداث تتعلّق بـرقم لا برسالة ولا بمحادثة، لذا تحمل number حيث تحمل الأخرى message أو chat. وnumber له الشكل نفسه الذي يعيده GET /v1/numbers/:id، الحالة والسلامة معًا، فيقرأ محلّل واحد الاثنين.
{
"type": "number.disconnected",
"occurredAt": "2026-09-16T10:05:00Z",
"reason": "SESSION_EXPIRED",
"number": {
"id": "chn_sales01",
"displayName": "Sales line",
"declaredNumber": "+212661234567",
"pairedNumber": "+212661234567",
"status": "CREDENTIALS",
"health": {
"phoneOnline": false,
"lastSeenAt": "2026-09-16T09:58:00Z",
"lastEventAt": "2026-09-16T10:05:00Z"
}
}
}reason لا يرافق سوى number.disconnected: شكل واحد لكل اسم حدث، وهي القاعدة التي تتبعها أصلًا أحداث الرسائل والمجموعات. عامِل number.disconnected بوصفه سببًا لمقاطعة شخص، وعامِل number.phone_offline بوصفه أمرًا يُنتظر انقضاؤه: فدمج الاثنين يقود العميل إلى إعادة ربط كاملة لأن هاتفه كان يشحن في الغرفة المجاورة.
يستطيع متصفّح يحمل جلسة لوحة تحكم أن يقرأ الأحداث الستة عشر نفسها مباشرة على GET /app/events. ولا يوجد مكافئ بمفتاح API، وذلك عن قصد: فـ EventSource في المتصفّح لا يستطيع إرسال ترويسة Authorization إطلاقًا، ما يعني أن تدفّقًا على الـ API سيضع مفتاح API في سلسلة استعلام وفي سجل المتصفّح وفي كل سجل وسيط. أما للخادم فالـ webhook هو قناة الدفع.
إصدارات الحمولة
تُضاف الحقول ولا يُعاد تسميتها أبدًا. ويحتفظ message.received بالحقول السبعة نفسها التي كانت له في الإصدار 1 (id وchannelId وchatId وfrom وsenderName وtext وtimestamp) بالأسماء والمعاني ذاتها، وتقف إلى جانبها type وstatus وattachments وreplyTo وlocation وcontacts وlinkPreview. والمعالِج المكتوب للإصدار 1 يظل يعمل، ولهذا ما زال تعليق الصورة يصل في text.
يظهر type مرتين ويعني شيئين: في أعلى المغلّف هو الحدث (message.read)، وداخل message هو نوع الرسالة (image). والاسم الداخلي مطابق لما في مورد الرسالة، فمحلّل واحد يقرأ الاثنين.
التحقّق من إعادة التوجيه
يحمل كل توجيه سرك المشترك في ترويسة x-sendveo-secret. قارِنه بالقيمة الموجودة في إعداد الـ webhook الخاص بك، وارفض أي شيء لا يطابقها.
app.post("/sendveo/inbound", (req, res) => {
const secret = req.header("x-sendveo-secret")
if (secret !== process.env.SENDVEO_WEBHOOK_SECRET) {
return res.status(401).end()
}
const { message } = req.body
handleInbound(message)
res.status(200).end()
})ما الذي يُعاد توجيهه
يُعاد توجيه كل حدث في الجداول أعلاه. ورسائلك الصادرة لا تُعاد إليك أبدًا بوصفها message.received، فذلك الحدث هو دائمًا رسالة حقيقية من شخص آخر. وقبل الإصدار 1.1 لم يكن موجودًا سوى message.received: والمعالِج الذي يتجاهل type غير معروف يبقى يعمل دون تغيير.