كيفية استقبال رسائل واتسابعلى خادمك —الدليل الكامل لعام 2026
مايو 21, 2026
·
14 دقيقة قراءة
في هذا الدليل: كيف يعمل مسار الـ webhook · بناء الـ endpoint (Node.js + Python) · التعامل مع تحدي التحقق · التحقق من توقيع HMAC-SHA256 · تحليل جميع أنواع الرسائل · التعامل مع تنزيل الوسائط · نمط طوابير الإنتاج · SocialHook كطبقة مُدارة · الأسئلة الشائعة
كيف يعمل مسار رسائل واتساب الواردة
تبدأ معظم البرامج التعليمية بالبرمجة قبل شرح البنية المعمارية. ولهذا السبب تحتوي معظم التطبيقات على أخطاء. إليك المسار الكامل — كل قفزة تمر بها الرسالة قبل أن تصل إلى منطق تطبيقك:
📱
العميليرسل رسالة
WhatsApp شبكة
☁️
Meta Cloud APIgraph.facebook.com
HTTP POST + توقيع HMAC
🔗
نقطة الويب هوك الخاصة بكرابط HTTPS
تحقق → قائمة انتظار → 200 OK
⚙️
المنطق الخاص بكAI, CRM, DB
يجب تحقق أربعة شروط ليعمل هذا المسار بموثوقية:
يجب أن يكون رابط نقطة النهاية الخاص بك متاحًا للعامة عبر HTTPS. لا تستخدم localhost، ولا HTTP. ترفض ميتا عناوين URL غير HTTPS ولا يمكنها الوصول إلى عناوين IP الخاصة.
يجب أن يستجيب رابط نقطة النهاية الخاص بك بالرمز 200 خلال 20 ثانية. أي استجابة أبطأ من ذلك ستجعل ميتا تعيد محاولة التسليم. وإذا كررت المحاولات مرات كافية، ستتوقف عن الإرسال إلى رابطك نهائيًا.
يجب أن تتحقق من توقيع HMAC-SHA256 في كل طلب. بدون هذا، يمكن لأي مهاجم يكتشف رابط الويب هوك الخاص بك إرسال رسائل مزيفة إلى نظامك.
قم بالتأكيد أولاً، ثم المعالجة ثانياً. أعد الرمز 200 فوراً، ادفع الحمولة إلى قائمة انتظار، واعمل بشكل غير متزامن. لا تقم أبدًا بأعمال ثقيلة بشكل متزامن داخل معالج الويب هوك.
الخطوة 1: بناء نقطة نهاية الويب هوك
يجب أن يتعامل رابط نقطة النهاية الخاص بك مع طريقتين من طرق HTTP على نفس العنوان: GET (تحدي التحقق لمرة واحدة من ميتا) و POST (أحداث الرسائل المباشرة). إليك الحد الأدنى من التطبيق العملي بلغتي Node.js وPython.
Node.js + Express
webhook.js
const express = require('express');
const crypto = require('crypto');
const app = express();
// CRITICAL: parse raw body BEFORE express.json()// You need the raw buffer for HMAC verification
app.use('/webhook', express.raw({ type: '*/*' }));
constVERIFY_TOKEN = process.env.WEBHOOK_VERIFY_TOKEN; // your own stringconstAPP_SECRET = process.env.WHATSAPP_APP_SECRET; // from Meta Developer Dashboard// GET — Meta verification challenge
app.get('/webhook', (req, res) => {
const mode = req.query['hub.mode'];
const token = req.query['hub.verify_token'];
const challenge = req.query['hub.challenge'];
if (mode === 'subscribe' && token === VERIFY_TOKEN) {
console.log('Webhook verified ✓');
res.status(200).send(challenge); // return challenge as plain text
} else {
res.sendStatus(403);
}
});
// POST — inbound message events
app.post('/webhook', (req, res) => {
// 1. Verify signature BEFORE anything elseif (!verifySignature(req)) {
return res.sendStatus(403);
}
// 2. Acknowledge immediately — NEVER process synchronously
res.sendStatus(200);
// 3. Parse and enqueue for async processingconst body = JSON.parse(req.body.toString());
enqueue(body); // your queue function — see Step 6
});
app.listen(3000, () => console.log('Webhook server running :3000'));
عند تسجيل رابط الويب هوك الخاص بك في لوحة مطوري ميتا، ترسل ميتا طلب GET لمرة واحدة للتحقق من أنك تتحكم في نقطة النهاية. هذه هي الخطوة التي تعثر فيها تقريبًا كل تنفيذ أولي — وعادةً لأن المطورين يعيدون JSON بدلاً من نص عادي، أو يعيدون كائن المعلمات الكامل بدلاً من قيمة التحدي فقط.
يتضمن طلب GET من ميتا ثلاث معلمات استعلام:
hub.mode — دائماً السلسلة النصية subscribe
hub.verify_token — السلسلة النصية التي تحددها في لوحة تحكم ميتا. أنت تختار هذه القيمة — اجعلها سراً عشوائياً، وليس كلمة قابلة للتخمين
hub.challenge — رقم عشوائي تريد ميتا استعادته كنص عادي
يجب أن يقوم رابط نقطة النهاية الخاص بك بـ: (1) التحقق من أن hub.mode === 'subscribe'، (2) التحقق من أن hub.verify_token يطابق القيمة المتوقعة لديك، (3) إعادة hub.challenge كنص للاستجابة مع Content-Type: text/plain وحالة 200. إذا أعدت JSON، أو أعدت قيمة خاطئة، أو أعدت حالة غير 200 — يفشل التحقق وتُعلِّم ميتا الويب هوك الخاص بك على أنه غير مُتحقَّق منه.
خطأ شائع: استخدام res.json({ challenge }) في Express بدلاً من res.send(challenge). تتوقع ميتا نصاً عادياً. تغليف التحدي بـ JSON يتسبب في فشل التحقق حتى لو أعاد خادمك الرمز 200.
الخطوة 3: التحقق من توقيع HMAC-SHA256 — الخطوة التي لا يتخطاها أحد في البيئة الإنتاجية
رابط الويب هوك الخاص بك عام. أي شخص يجده يمكنه إرسال حمولات مزيفة عبر POST إلى خادمك. بدون التحقق من التوقيع، يمكن تغذية وكيل الذكاء الاصطناعي الخاص بمحادثات مُلفَّقة، ويمكن تسميم نظام إدارة العلاقات مع العملاء (CRM) بجهات اتصال مُختلَقة، ويمكن تشغيل منطقك بواسطة مهاجم حسب رغبته.
توقِّع ميتا كل طلب POST باستخدام App Secret الخاص بك (الموجود في Meta Developer Dashboard → App Settings → Basic). يوجد التوقيع في رأس X-Hub-Signature-256، بصيغة sha256=<hex_digest>. الـ digest هو HMAC-SHA256 لبايتات جسم الطلب الخام باستخدام App Secret كمفتاح.
Node.js
verifySignature.js
functionverifySignature(req) {
const sigHeader = req.headers['x-hub-signature-256'];
if (!sigHeader) return false;
// Remove 'sha256=' prefixconst receivedSig = sigHeader.slice(7); // everything after 'sha256='// req.body MUST be the raw buffer — see express.raw() setup aboveconst expectedSig = crypto
.createHmac('sha256', APP_SECRET)
.update(req.body) // raw Buffer, NOT parsed JSON
.digest('hex');
// timingSafeEqual يمنع هجمات التوقيتtry {
return crypto.timingSafeEqual(
Buffer.from(receivedSig, 'hex'),
Buffer.from(expectedSig, 'hex')
);
} catch {
return false; // الأطوال غير المتطابقة تُسبب خطأ — تعامل معها كغير صالحة
}
}
module.exports = { verifySignature };
حرج: استخدم البايتات الخام، وليس JSON المُحلَّل. إذا استدعيت JSON.parse() قبل حساب HMAC، فلن يتطابق الـ digest أبدًا — يمكن أن يُغيِّر تسلسل JSON المسافات البيضاء وترتيب المفاتيح. يجب حساب HMAC على البايتات الدقيقة التي وصلت في جسم الطلب. في Express، يعني هذا إعداد express.raw() على مسار الويب هوك الخاص بك قبل أي محلل جسم آخر. في FastAPI، استدعِ await request.body() قبل أي إزالة تسلسل لـ JSON.
الخطوة 4: التطوير المحلي — تعريض localhost لميتا
لا يمكن لميتا الوصول إلى http://localhost:3000. أثناء التطوير، تحتاج إلى عنوان URL عام عبر HTTPS يُوجِّه الحركة إلى خادمك المحلي. خياران موثوقان:
Terminal
خيارات النفق
# الخيار أ: ngrok (الأكثر شيوعًا، يتطلب حسابًا مجانيًا)
ngrok http 3000
# → https://a1b2c3d4.ngrok-free.app ← الصق هذا في لوحة تحكم ميتا# الخيار ب: Cloudflare Tunnel (مجاني، بلا حد زمني، يتطلب cloudflared)
cloudflared tunnel --url http://localhost:3000
# → https://random-words.trycloudflare.com ← الصق هذا# عنوان الويب هوك الكامل للتسجيل في لوحة تحكم ميتا:# https://your-tunnel-url.ngrok-free.app/webhook
سجِّل عنوان النفق في لوحة مطوري ميتا (WhatsApp → Configuration → Webhooks). عند إعادة تشغيل ngrok يُولِّد عنوانًا جديدًا — حدِّث لوحة التحكم في كل مرة. يستمر Cloudflare Tunnel عبر إعادة التشغيل عند استخدام أنفاق مُسمَّاة. انتقل إلى عنوان نطاقك الإنتاجي قبل النشر الفعلي.
الصق عنوان الويب هوك الخاص بك عبر HTTPS (مثلاً https://yourdomain.com/webhook)
أدخل Verify Token الخاص بك — السلسلة النصية الدقيقة التي حددتها في VERIFY_TOKEN
انقر على Verify and Save — تُطلِق ميتا تحدي GET فورًا. يجب أن يستجيب خادمك خلال ~5 ثوانٍ.
بعد الحفظ، انقر على Manage بجانب الويب هوك وفعِّل اشتراك الحقل messages. بدون هذا، لن تدفع ميتا أحداث الرسائل الواردة إلى نقطة النهاية الخاصة بك.
الخطوة 6: تحليل حمولة الويب هوك لـ Cloud API
بمجرد اكتمال التحقق، يُفعِّل كل رسالة واردة من العميل طلب POST إلى نقطة النهاية الخاصة بك. إليك الهيكل الكامل لحدث رسالة نصية من Cloud API — وكيفية التنقل فيه:
الرسالة التي تهتم بها مدفونة عند body.entry[0].changes[0].value.messages[0]. هذا التداخل يفاجئ الجميع. يصل حدث تحديث الحالة (إيصال التسليم) في نفس المغلَّف ولكن يحتوي على مصفوفة statuses بدلاً من messages. إليك نمط الاستخراج:
Node.js
parseWebhook.js
functionparseWebhookEvent(body) {
const value = body?.entry?.[0]?.changes?.[0]?.value;
if (!value) return null;
const phoneNumberId = value.metadata?.phone_number_id;
// الرسائل الواردةif (value.messages?.length) {
const msg = value.messages[0];
return {
kind: 'message',
phoneNumberId, // أي من أرقامك استلم الرسالة
from: msg.from, // المرسل — بدون بادئة +
messageId: msg.id,
timestamp: parseInt(msg.timestamp, 10), // تحويل سلسلة→عدد صحيح
type: msg.type, // 'text'|'image'|'audio'|إلخ
raw: msg, // كائن الرسالة الكامل
};
}
// تحديثات حالة التسليم/القراءةif (value.statuses?.length) {
const s = value.statuses[0];
return {
kind: 'status',
messageId: s.id,
status: s.status, // 'sent'|'delivered'|'read'|'failed'
recipient: s.recipient_id,
};
}
return null;
}
الخطوة 7: التعامل مع جميع أنواع رسائل واتساب
يخبرك الحقل type بأي خاصية يجب قراءتها. كل نوع له هيكل مختلف. إليك الخريطة الكاملة:
💬
text
رسالة نصية قياسية من العميل
اقرأ: msg.text.body
🖼️
image
صورة أرسلها العميل. قد تتضمن تعليقًا اختياريًا.
اقرأ: msg.image.id → تنزيل
🎵
audio
ملاحظة صوتية أو ملف صوتي. voice: true إذا تم تسجيلها داخل التطبيق.
اقرأ: msg.audio.id → تنزيل
📄
document
ملف PDF أو Word أو Excel أو ملف آخر. يتضمن اسم الملف.
اقرأ: msg.document.id، .filename
🎬
video
ملف فيديو. قد يتضمن تعليقًا.
اقرأ: msg.video.id → تنزيل
📍
location
موقع تم تحديده بواسطة العميل. يتضمن خط العرض/خط الطول واسمًا اختياريًا.
الخطوة 8: تنزيل الوسائط الواردة — الجلب ذو الخطوتين
عندما يرسل العميل صورة أو ملاحظة صوتية أو مستندًا، لا تحتوي حمولة الويب هوك على الملف — بل تحتوي على معرف وسائط. يجب عليك إجراء استدعاء واجهة برمجة تطبيقات منفصل لحل المعرف إلى عنوان تنزيل مؤقت، ثم جلب الملف الفعلي. ينتهي صلاحية عنوان التنزيل بعد 5 دقائق — قم بالتنزيل فورًا وتخزينه على بنيتك التحتية الخاصة.
Node.js
downloadMedia.js
constGRAPH_URL = 'https://graph.facebook.com/v21.0';
constACCESS_TOKEN = process.env.WHATSAPP_ACCESS_TOKEN;
// الخطوة 1: حل معرف الوسائط → عنوان تنزيل مؤقتasync functiongetMediaUrl(mediaId) {
const res = awaitfetch(
`${GRAPH_URL}/${mediaId}`,
{ headers: { Authorization: `Bearer ${ACCESS_TOKEN}` } }
);
const data = await res.json();
return data.url; // ينتهي خلال 5 دقائق — نزِّل الآن
}
// الخطوة 2: جلب بايتات الملف الفعليةasync functiondownloadMedia(mediaUrl) {
const res = awaitfetch(mediaUrl, {
headers: { Authorization: `Bearer ${ACCESS_TOKEN}` }
});
return Buffer.from(await res.arrayBuffer());
}
// تجميع: حل المعرف → تنزيل → تخزين على S3/GCS/خادمكasync functiondownloadAndStore(mediaId, filename) {
const mediaUrl = awaitgetMediaUrl(mediaId);
const buffer = awaitdownloadMedia(mediaUrl);
awaituploadToStorage(buffer, filename ?? mediaId);
// خزِّن mediaId أو عنوان التخزين الخاص بك في قاعدة البيانات لسجل المحادثة
}
الخطوة 9: بنية قائمة الانتظار — لا تقم بالمعالجة أبدًا داخل معالج الويب هوك
هذا هو نمط الإنتاج الذي يفصل بين تنفيذ تجريبي وآخر يصمد أمام الحركة الحقيقية. القاعدة مطلقة: يجب أن يعيد معالج الويب هوك الخاص بك الرمز 200 خلال 20 ثانية. إذا استدعيت نموذج لغة كبير، أو استعلمت عن نظام إدارة علاقات العملاء، أو نزّلت وسائط بشكل متزامن داخل المعالج، فستنتهي مهلتك في النهاية تحت الحمل، وستعيد ميتا المحاولة، وستعالج أحداثًا مكررة.
النمط الصحيح: التأكيد فورًا، دفع الحمولة الخام إلى قائمة انتظار، المعالجة بشكل غير متزامن في عامل. إليك نمط BullMQ الحد الأدنى (مدعوم بـ Redis):
Node.js + BullMQ
queue.js
const { Queue, Worker } = require('bullmq');
const redis = { host: '127.0.0.1', port: 6379 };
const whatsappQueue = newQueue('whatsapp', { connection: redis });
// يُستدعى داخل معالج POST الخاص بك — فوري، خفيفasync functionenqueue(payload) {
await whatsappQueue.add('process-event', payload, {
attempts: 3, // إعادة المحاولة حتى 3 مرات عند فشل العامل
backoff: { type: 'exponential', delay: 2000 },
removeOnComplete: 100, // الاحتفاظ بآخر 100 وظيفة مكتملة لأغراض التصحيح
removeOnFail: 200,
});
}
// يعمل العامل في عملية منفصلةconst worker = newWorker('whatsapp', async (job) => {
const event = parseWebhookEvent(job.data);
if (!event) return;
if (event.kind === 'message') {
awaithandleMessage(event.raw); // منطق الذكاء الاصطناعي / نظام إدارة علاقات العملاء الخاص بك هنا
}
if (event.kind === 'status') {
awaitupdateMessageStatus(event.messageId, event.status);
}
}, { connection: redis, concurrency: 10 });
إزالة التكرار: قد تُسلِّم ميتا نفس الحدث أكثر من مرة إذا كان خادمك بطيئًا في التأكيد. خزِّن قيم msg.id التي تمت معالجتها في Redis مع TTL قصير (مثلاً 24 ساعة). قبل معالجة وظيفة، تحقق مما إذا كان msg.id موجودًا بالفعل في مجموعة إزالة التكرار الخاصة بك. إذا كان موجودًا، تخطَّ المعالجة. إذا لم يكن، أضفه وقم بالمعالجة. هذا يمنع الردود المزدوجة من وكيل الذكاء الاصطناعي الخاص بك والسجلات المكررة في نظام إدارة علاقات العملاء.
بديل: تخطَّ كل ما سبق باستخدام SocialHook
كل ما سبق — التعامل مع تحدي التحقق، والتوقيع بـ HMAC-SHA256، واستخراج الحمولة من entry[0].changes[0].value المتداخل، وحل معرف الوسائط، ومنطق إعادة المحاولة، وإزالة التكرار — هو عمل بنية تحتية. هذا ليس منتجك. هذا هو السقالات التي يعمل عليها منتجك.
يستبدل SocialHook هذه الطبقة بأكملها. تربط رقم واتساب للأعمال الخاص بك بـ SocialHook، تلصق عنوان الويب هوك الخاص بخادمك في لوحة تحكم SocialHook، ويقوم SocialHook بـ:
إعادة محاولة التسليم إلى نقطة النهاية الخاصة بك حتى 3 مرات مع تأخير أسي إذا أعاد خادمك رمزًا غير 200
تسجيل كل محاولة تسليم مع الطابع الزمني، ورمز الحالة، وزمن الاستجابة
التسليم إلى نقطة النهاية الخاصة بك في أقل من 50 مللي ثانية
تتلقى نقطة النهاية الخاصة بك هذا بدلاً من الحمولة الخام المتداخلة من ميتا:
JSON
socialhook-normalized-payload.json
{
"platform": "whatsapp",
"event": "message.received",
"timestamp": 1747231892, // عدد صحيح — تم تحليله بالفعل"from": "+44 7700 900 456", // E.164 — تمت إضافة بادئة +"conversation_id": "conv_8j3k...",
"message": {
"type": "text",
"body": "Hello, is this working?",
"id": "wamid.HBgL..."
},
"signature_verified": true,
"delivery": {
"attempt": 1,
"latency_ms": 41
}
}
لا تنقل عبر مستويات متداخلة. لا parseInt(timestamp). لا بادئة + مفقودة في رقم المرسل. لا عبارات switch خاصة بالمنصة — يصل نفس شكل الحمولة سواء راسلك العميل عبر واتساب أو فيسبوك أو إنستغرام. معالج ويب هوك واحد، ثلاث قنوات. التكلفة الكاملة هي $50/شهر ثابت — أقل من ساعة من وقت الهندسة لبناء المكافئ وصيانته من الصفر.
الأسئلة الشائعة
أسئلة شائعة
كيف أستقبل رسائل واتساب على خادمي؟
تحتاج إلى رقم واتساب للأعمال على Cloud API، ونقطة نهاية HTTPS متاحة للعامة، وتسجيل تلك النقطة كويب هوك الخاص بك في لوحة مطوري ميتا. تُطلِق ميتا بعد ذلك طلب HTTP POST إلى نقطة النهاية الخاصة بك عند كل رسالة واردة. يجب أن يستجيب خادمك بالرمز 200 خلال 20 ثانية. راجع الإعداد الكامل المكون من 9 خطوات أعلاه.
لماذا يفشل التحقق من HMAC باستمرار؟
في الغالب العظمى لأنك تحسب HMAC على JSON المُحلَّل بدلاً من بايتات جسم الطلب الخام. في Express، يجب إعداد express.raw() على مسار الويب هوك الخاص بك قبل أي محلل جسم آخر. في FastAPI، استدعِ await request.body() قبل إزالة التسلسل. تحقق أيضًا من أنك تستخدم App Secret الخاص بك (من App Settings → Basic) وليس رمز الوصول كمفتاح HMAC — هذان قيمتان مختلفتان.
كيف أختبر الويب هوك الخاص بي محليًا قبل النشر؟
استخدم أداة نفق. ngrok (ngrok http 3000) هو الأكثر شيوعًا — يُولِّد عنوان HTTPS عام يُعيد التوجيه إلى المنفذ المحلي الخاص بك. Cloudflare Tunnel (cloudflared tunnel --url http://localhost:3000) بديل مجاني بلا حدود زمنية للجلسة. الصق العنوان المُولَّد في حقل الويب هوك بلوحة مطوري ميتا. تذكر تحديثه عند إعادة تشغيل ngrok لأن العنوان يتغير.
تواصل ميتا إعادة محاولة الويب هوك الخاص بي — لماذا؟
ثلاثة أسباب: (1) أعاد خادمك رمز حالة غير 200 — أخطاء التحقق من التوقيع تُعيد 403، واستثناءات المعالجة تُعيد 500. (2) استغرق خادمك أكثر من 20 ثانية للرد — انقل المعالجة إلى قائمة انتظار غير متزامنة وأعد 200 فورًا. (3) كانت نقطة النهاية الخاصة بك معطلة عندما أرسلت ميتا الحدث — تأكد من أن خادمك يعمل دائمًا أو استخدم التسليم المُدار من SocialHook مع إعادة المحاولة التلقائية.
ما الفرق بين حمولة ميتا الخام والحمولة المُطبعَة من SocialHook؟
تُغلِّف Cloud API من ميتا كل حدث في هيكل متداخل: body.entry[0].changes[0].value.messages[0]. الطابع الزمني سلسلة نصية، والمرسل لا يحتوي على بادئة +، والتنسيق يختلف عن вебهوكات Facebook Messenger وInstagram. يستخرج SocialHook الرسالة، ويطبِّع المرسل إلى تنسيق E.164، ويحوِّل الطابع الزمني إلى عدد صحيح، ويُسلِّم نفس هيكل JSON المسطَّح عبر قنوات ميتا الثلاث. تكتب محلِّلًا واحدًا ويعمل مع واتساب وفيسبوك وإنستغرام.
كيف أتعامل مع الوسائط الواردة (صور، ملاحظات صوتية، مستندات)؟
تحتوي حمولة الويب هوك على معرف وسائط، وليس الملف نفسه. يجب عليك: (1) استدعاء GET https://graph.facebook.com/v21.0/{media_id} برمز الوصول الخاص بك للحصول على عنوان تنزيل مؤقت، (2) جلب الملف من ذلك العنوان (أيضًا برمز الوصول في رأس Authorization)، (3) تخزين الملف على خادمك الخاص أو تخزين سحابي. ينتهي صلاحية العنوان المؤقت خلال حوالي 5 دقائق — نزِّل فورًا. راجع كود Node.js الكامل في قسم الوسائط أعلاه.
لقد قرأت التنفيذ الكامل. إذا كنت تفضل عدم صيانة التحقق من HMAC، ومنطق إعادة المحاولة، وتطبيع الحمولة بنفسك — يتعامل SocialHook مع كل ذلك. الصق عنوان نقطة النهاية الخاصة بك. استلم JSON نظيف. $50/شهر ثابت.