إرسال رسالة واحدة عبر API في واتساب وتيليجرام إلى عميل بعينه: طلب واحد، بنصه ومرفقه، دون حملة. مناسب لرموز التحقق والتذكيرات.
بعض الرسائل لا تحتمل الانتظار حتى حملة المساء. عميل دفع ثمن طلبه. أو حجز موعدًا. أو طلب رمز التحقق بينما الموظف ما زال معه على الهاتف.
رسالة كهذه موجّهة إلى شخص واحد، وتفقد قيمتها بعد دقائق، ولن تتكرر. وتجهيز حملة من أجلها أمر غريب: جمهور من رقم واحد، ونص لحالة واحدة، وكل ذلك من أجل رسالة واحدة.
لذلك أصبحت في API الآن نقطة نهاية مستقلة. طلب HTTP واحد يرسل رسالة واحدة إلى شخص بعينه، بنصها ومرفقها. وهنا نتحدث عن الإرسال الفردي فقط، أما الحملات فتناولناها في مقال واجهة API للإرسال الجماعي.
كيف يبدو الإرسال عبر API
المخطط بسيط: حدث في نظامك ← طلب إلى API ← رسالة عند العميل.
نقطة النهاية كلها طلب واحد POST /v1/message، وفيه ثلاثة أمور: من أي حساب تُرسل، وإلى من، وماذا. المفتاح هو نفسه الذي تستخدمه للحملات، من قسم API في لوحة التحكم:
curl -X POST https://api.wsender.io/v1/message \
-H "Authorization: key YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"channelId": "ACCOUNT_ID",
"phone": "+971 50 123 4567",
"message": { "kind": "text", "text": "تم دفع طلبك رقم 84421، بانتظارك غدًا الساعة 12:00." },
"idempotencyKey": "order-84421-paid"
}'
ويصل في الرد كائن الرسالة بمعرّفها وحالتها:
{
"id": "0f3c9a6e-4b12-4d8a-9f31-6c5a71e08d42",
"mid": null,
"status": "Queued",
"channelId": "ACCOUNT_ID",
"target": "Phone",
"phone": "971501234567",
"dialogId": null,
"errorReason": null,
"sendDate": null,
"failDate": null,
"wait": null,
"expiresAt": "2026-09-07T21:15:04Z",
"idempotencyKey": "order-84421-paid"
}
يُؤخذ معرّف الحساب من قائمة الحسابات المتصلة عبر GET /v1/channel، أما idempotencyKey فهو مفتاحك الخاص للطلب، وسنعود إليه لاحقًا. يمتلئ الحقل dialogId حين تعثر الخدمة على المحادثة مع هذا الشخص، وبه يمكنك لاحقًا الرد في المحادثة نفسها. وإن كنت قد راسلته من قبل، يأتي ممتلئًا مباشرة في هذا الرد. ويظهر mid بعد الإرسال، وهو معرّف الرسالة داخل المحادثة. يشرح wait ما الذي يُبقي الرسالة في الطابور، ويحدد expiresAt آخر موعد تنتظر فيه الرسالة غير المرسلة دورها: بعده تنتهي بالسبب Expired.
يصل الرد على الطلب فورًا، لكن معناه أن الرسالة قُبلت في الطابور، ولم تُسلَّم بعد. يرسلها حسابك المتصل، وهو حساب حقيقي يعمل من تطبيق المراسلة. يستغرق ذلك ثواني، وقد يصل إلى دقيقتين مع رقم لم تراسله من قبل: تطبيقات المراسلة لا تحب التحقق السريع من الأرقام. أما البوت فأسرع، لأنه يرسل مباشرة عبر Bot API دون تطبيق يعمل على هاتف.
هذا الفرق مهم للموظف الذي يتحدث مع العميل. العبارة الصحيحة على الهاتف: «جارٍ إرسال الرسالة إليك الآن»، وليس «وصلتك بالفعل».
إلى من تُرسل في واتساب وتيليجرام وبوتات تيليجرام
يُحدَّد المستلم بإحدى الطرق التالية، والطريقة هي التي تحدد ما إذا كنت تحتاج إلى رقم أصلًا:
- رقم الهاتف بأي صيغة معتادة. الحالة الأكثر شيوعًا: الرقم موجود لديك في بطاقة العميل.
- اسم المستخدم في تيليجرام، إذا كان حسابك يعرفه من مراسلة سابقة.
- المحادثة بمعرّفها من API أو بعنوانها داخل تطبيق المراسلة نفسه. ويجب أن يعرف حسابك هذه المحادثة مسبقًا: الطلب يرفض العنوان غير المعروف ولا يجرّبه على سبيل التخمين.
في الطلب أربعة حقول لهذا الغرض، ولا يُملأ إلا واحد منها، فالخدمة لا تخمّن إلى من أردت أن تكتب:
"phone": "+971 50 123 4567"
"username": "@ahmed_alharbi"
"dialogId": "DIALOG_ID_FROM_API"
"dialogMid": "123456789"
الخيار الأخير هو عنوان المحادثة داخل تطبيق المراسلة نفسه، أي chat_id لدى بوتات تيليجرام. وهو مناسب حين يخزّن نظامك عناوين تطبيق المراسلة أصلًا ولا يحتاج إلى الاحتفاظ بمعرّفاتنا.
مع البوتات لا تعمل إلا العنونة بالمحادثة. والسبب ليس في الخدمة: قد لا يكون لدى مشترك البوت رقم ظاهر ولا اسم مستخدم، لكن المحادثة موجودة دائمًا. ولا يستطيع البوت أن يبدأ المراسلة على أي حال، وهذا قيد تفرضه المنصة لا نحن.
ما الذي يمكن إرساله
نص، أو صورة، أو فيديو، أو ملف صوتي، أو مستند، أو بطاقة جهة اتصال. ويدعم النص صيغًا بديلة بين أقواس مربعة: [مرحبًا|أهلًا وسهلًا] يتحول إلى إحدى الصيغتين، كي لا تبدو الرسائل نسخًا مكررة.
وهنا فخ يُستحسن أن تعرفه قبل أول طلب: الأقواس المربعة تُعامَل كصيغ بديلة حتى لو لم يكن بداخلها سوى خيار واحد. النص [مهم] رمزك 1234 يصل إلى العميل هكذا: مهم رمزك 1234، بلا أقواس. إن كانت قوالبك تستخدم الأقواس للتنسيق، فاستبدلها بعلامة أخرى.
يُرفع الملف مرة واحدة بطلب منفصل، ثم يُمرَّر بمعرّفه في أي عدد من الرسائل. قائمة أسعار واحدة تُرسل إلى مئة عميل تشغل مساحة ملف واحد. والقيود بسيطة: حتى 10 ميغابايت للملف، والملف غير المستخدم يُحفظ يومًا واحدًا ثم يُحذف تلقائيًا.
الرفع طلب نموذج عادي (form) يحمل الملف، والحقل الأهم في الرد هو id:
curl -X POST https://api.wsender.io/v1/file \
-H "Authorization: key YOUR_TOKEN" \
-F "file=@price-2026.pdf"
إن كان الملف موجودًا على رابط عام، فبدل النموذج استخدم POST /v1/file/url مع الحقل url، وسينزّل الخادم الملف بنفسه. بعدها يوضع المعرّف في الرسالة، ويُحدَّد النوع بالحقل kind: text أو image أو video أو audio أو file أو contact. وللصورة والفيديو والمستند تعليق caption، ولأي مرفق اسم fileName يراه المستلم:
{
"channelId": "ACCOUNT_ID",
"phone": "+971 50 123 4567",
"message": {
"kind": "file",
"fileId": "UPLOADED_FILE_ID",
"fileName": "قائمة أسعار الموسم",
"caption": "مرحبًا، هذه قائمة الأسعار التي تحدثنا عنها. الأسعار سارية حتى نهاية الشهر."
},
"idempotencyKey": "price-request-1187"
}
الاسم اختياري: من دونه يُستخدم اسم الملف المرفوع، والامتداد يُضاف في كل الأحوال، فلا حاجة إلى تمريره. وتُرسل الصورة بالطريقة نفسها مع "kind": "image"، أما بطاقة جهة الاتصال فلا تحتاج إلى ملف: { "kind": "contact", "phone": "+971 50 000 0000" }.
الاستثناء الوحيد: لا يمكن إرسال بطاقة جهة اتصال من بوت، فالخدمة ترسلها من حساب المراسلة فقط. يُرفض الطلب فورًا، فلا يضيع بصمت أثناء الإرسال.
كيف تعرف أن الرسالة وصلت
أخبرك الرد على الطلب بأن الرسالة قُبلت. بعد ذلك تخبرك الخدمة بنفسها بما انتهى إليه الأمر، عبر ثلاثة أحداث في الويب هوك:
message.sent. خرجت الرسالة إلى تطبيق المراسلة.message.failed. تعذّر الإرسال، والسبب مذكور في الحدث: المستلم حظر الحساب، أو لا يجوز أن تبدأ أنت مراسلته، أو انفصل حساب الإرسال. وقد يكون الإرسال قد اصطدم لبضع دقائق بعطل مؤقت في تطبيق المراسلة حتى نفدت المحاولات: رسالة كهذه يكفي أن تضعها في الطابور من جديد.message.notfound. المستلم غير موجود في تطبيق المراسلة: لا رقم كهذا ولا حساب. وهذا حدث مستقل، وليس نوعًا من الأخطاء: لا شيء يحتاج إلى إصلاح، لكن يستحسن أن تضع علامة على جهة الاتصال في قاعدتك.
الاشتراك في هذه الأحداث صريح. حدّد عنوان الويب هوك واذكر هذه الأحداث في مرشّحه: من دون ذلك لن تصلك، حتى لو كان الويب هوك مضبوطًا بالفعل على أحداث الحملات. وهذا مقصود. من يرسل مئة رسالة في اليوم يستفيد من حدث لكل رسالة، أما من يدير حملات لعشرات الآلاف من المستلمين فلا ينبغي أن يتلقى هذا السيل مصادفة.
يحمل الحدث: معرّف الرسالة، وحساب الإرسال، والمستلم بالصيغة التي حدّدته بها، والحالة، وسبب الإخفاق، والتواريخ. والأهم: مفتاحك الخاص، المفتاح ذاته الذي مرّرته مع الطلب. به تجد الطلب المقابل في نظامك، فلا تحتاج إلى تخزين معرّفاتنا أصلًا.
هكذا يبدو حدث الإخفاق حين يكون المستلم قد حظر حسابك:
{
"id": "7d1e0b58-3a44-4f2c-9c81-15b0f7a9d6e3",
"type": "message.failed",
"date": "2026-09-07T09:15:42Z",
"payload": {
"message": {
"id": "0f3c9a6e-4b12-4d8a-9f31-6c5a71e08d42",
"channelId": "ACCOUNT_ID",
"phone": "971501234567",
"status": "Failed",
"errorReason": "Blocked",
"failDate": "2026-09-07T09:15:41Z",
"idempotencyKey": "order-84421-paid"
}
}
}
السبب يدلّك على الخطوة التالية. Blocked وRestrictedSend: لا يمكن مراسلة هذا الشخص من هذا الحساب. ChannelDisconnected: انفصل الحساب، فأعد ربطه وأرسل من جديد. Expired وUnavailable: تعذّر الإرسال خلال المدة المحددة، ويمكن وضع الرسالة في الطابور مجددًا بمفتاح جديد.
عند بناء معالج الأحداث، انتبه إلى ثلاثة أمور:
- قد يصل الحدث نفسه مرتين. مثلًا، استلمه خادمك لكن الرد ضاع في الطريق. تحقّق من
idالحدث ولا تنفّذ الإجراء في نظامك مرة ثانية. - الإلغاء الذي تجريه بنفسك لا يولّد حدثًا. ألغيت الرسالة وتلقيت الرد على طلب الإلغاء، فلا معنى لإبلاغك بإجرائك مرة أخرى.
- يمكن التحقق من النتيجة عبر API. تعالج الخدمة أعطال الإرسال المؤقتة بنفسها. وإن تعذّر إرسال الرسالة خلال المدة المحددة، تحصل على الحالة
Failed، ويُنشأ للويب هوك النشط حدثmessage.failedمع السبب. وإن كان الخادم المستقبِل لديك غير متاح، فقد لا يصل الحدث، لكن النتيجة الحالية تبقى متاحة عبرGET /v1/message/{id}.
تتكرر محاولات تسليم الويب هوك بفواصل متزايدة، لكن لا يُحفظ أي حدث أكثر من يوم واحد. وبعد يوم من الأخطاء دون تسليم ناجح، يُعلَّق الويب هوك ولا تتراكم الأحداث الجديدة.
تحقّق من health في رد GET /v1/webhook، وأصلح الخادم المستقبِل، ثم استدعِ POST /v1/webhook/resume. الفحص الناجح يستأنف تسليم الأحداث المحفوظة؛ أما ما فات خلال فترة التعليق وما انتهت مدته فلا يُستعاد.
إن لم ترغب في إعداد الويب هوك، يمكنك طلب حالة الرسالة بمعرّفها: GET /v1/message/{id} يعيد البنية نفسها التي يعيدها رد الإرسال، مع قيمة status الحالية: Queued أو Sending أو Sent أو Failed أو NotFound. لكن لنكن صريحين: مباشرة بعد الطلب ستكون الحالة «في الطابور»، ولن تعرف النتيجة من استعلام واحد، وستضطر إلى السؤال مجددًا. ولا يوجد في نقطة النهاية وضع «انتظر حتى تُرسل»، وهذا مقصود: إبقاء طلب HTTP مفتوحًا لدقيقة أسوأ من إرسال حدث. لذلك يبقى الويب هوك هنا الطريق الموصى به.
الرد على الرسائل الواردة عبر API نفسها
ولا تقتصر نقطة النهاية على الرسائل الصادرة. تُبلغك الخدمة عن كل رسالة جديدة من العميل بحدث message.received، وفيه النص والمرسل والمحادثة. بعدها تردّ بطلب الإرسال نفسه، وتحدّد المحادثة الواردة في الحدث مستلمًا. وبهذا تبني على نقطة نهاية واحدة تواصلًا في الاتجاهين: صندوق وارد خاص بك، أو روبوت محادثة، أو ربطًا مع نظام CRM. وإن لم تكن بحاجة إلى صندوق وارد خاص، فيكفي الموظفين تطبيق Dialogs الذي يُفتح من لوحة التحكم: فيه المراسلات نفسها، والردود المرسلة عبر API تحمل توقيع «API».
ويمكن أن يأتي الرد مقتبِسًا رسالة بعينها، كما تفعل في تطبيق المراسلة حين تردّ على عبارة من منتصف الحديث. ولهذا الغرض يُمرَّر في الطلب معرّف الرسالة المقتبسة: تجده في حدث الرسالة الواردة، وفي سجل المحادثة الذي تعيده API.
إنه POST /v1/message نفسه، لكن المستلم هو المحادثة، ويُضاف الحقل replyToMid:
{
"channelId": "ACCOUNT_ID",
"dialogId": "DIALOG_ID_FROM_EVENT",
"replyToMid": "CLIENT_MESSAGE_MID",
"message": { "kind": "text", "text": "نعم، متوفر. نحجزه لك حتى المساء؟" },
"idempotencyKey": "reply-5c2e-1"
}
يأتي المعرّفان كلاهما في حدث الرسالة الواردة: المحادثة في payload.dialog.id، والرسالة نفسها في payload.message.mid. ولا يمكن اقتباس إلا رسالة من المحادثة نفسها. والخدمة لن تقبل رسالة من محادثة أخرى: يجري التحقق لحظة الطلب، فلا يضيع الخطأ بصمت أثناء الإرسال.
ويمكن اقتباس أي نوع من الرسائل، في حسابات واتساب وتيليجرام وفي بوتات تيليجرام على حد سواء.
قد تُحذف الرسالة الأصلية بعد طلبك. عندها يرسل حساب المراسلة ردك رسالة عادية دون اقتباس: لا داعي لخسارة النص بسبب التنسيق. أما البوت فلا يستطيع ذلك: تنتهي الرسالة بخطأ، وسببه يوضح المطلوب، أي إعادة الإرسال دون اقتباس.
تكرار الطلب لا ينشئ رسالة ثانية
للرسائل الإجرائية مشكلة خاصة: انقطعت الشبكة لحظة الرد، فأعاد الكود إرسال الطلب، وتلقى العميل رسالتين متطابقتين.
مرّر مع الطلب مفتاحك الفريد، وعندها يعيد الطلب المكرر بالمفتاح نفسه الرسالة ذاتها بحالتها الحالية ولا يرسلها مرة ثانية. هذه آلية idempotency المعروفة، وهي تعمل هنا على مستوى الرسالة نفسها.
ما دامت الرسالة تنتظر في الطابور، يمكن إلغاؤها بطلب منفصل: DELETE /v1/message/{id}. وما إن يتسلّمها حساب الإرسال حتى يفوت أوان الإلغاء: يعود الرد 409 مع الحالة الحالية في جسم الرد، ويحسم الإرسال نفسه النتيجة. والرسالة الملغاة تظهر بعد ذلك بالحالة Failed والسبب Canceled، فلا توجد حالة «ملغاة» مستقلة.
ما لا تسمح به الخدمة عند الإرسال عبر API
نصارحك بالحدود كي لا تبني ما لا حاجة إليه.
- فحص المواضيع المحظورة يعمل هنا أيضًا. تُفحص الرسالة لحظة الطلب، والمرفوضة لا تدخل الطابور، والسبب مذكور في الرد. الإرسال المباشر لا يتيح الالتفاف على المراجعة.
- الحد اليومي للباقة مشترك مع الحملات. تستهلكه الرسائل المباشرة بالقدر نفسه: كل رسالة مرسلة تساوي رسالة واحدة من الباقة، ولا حد مستقلًا لنقطة النهاية. ولا رسوم إضافية مقابل التجاوز: نفاد الحد يعني رفض الطلب، لا إرسالًا مدفوعًا. ويقلّ الرصيد المتبقي أيضًا بقدر ما ينتظر في طابور المشروع.
- بين عمليات الإرسال من الحساب نفسه فاصل من بضع ثوانٍ. هذا يحمي الحساب من سيل رسائل يُرسَل في حلقة برمجية: المحادثة الحية مع العميل لا تتأثر به، أما محاولة إرسال ألف رسالة عبر نقطة النهاية هذه فستصطدم به.
- لا توفّر الخدمة أرقامًا من عندها. تخرج الرسالة من حسابك المتصل أو من البوت الخاص بك: تبقى سمعة الرقم لك، وتصلك ردود العملاء في تطبيق المراسلة المعتاد.
ما التالي
تعمل نقطة النهاية على الباقات العادية، وهي الآن في مرحلة الاختبار: لا سعر مستقلًا لها حاليًا، والرسائل تُحتسب كرسائل الباقة العادية. في الباقة المجانية 15 رسالة يوميًا، وهذا يكفي لبناء التكامل.
ابدأ بطلب واحد إلى رقمك: اربط حسابًا، واحصل على مفتاح من قسم API في لوحة التحكم، وأرسل لنفسك رسالة تجريبية. بعدها أضف الاشتراك في الأحداث، وضع الاستدعاء في الموضع الذي يقع فيه الحدث في نظامك.
الوصف الكامل لنقاط النهاية وإمكانية تنفيذ الطلبات من المتصفح مباشرة تجدهما في توثيق API، ومواصفات توليد العميل في ملف OpenAPI.