في الصفحة دي

ده مرجع الإصدار القديم (v1). الإصدار الجديد v2 فيه مفاتيح بصلاحيات وكل الموارد في واجهة واحدة — افتح توثيق v2 أو شوف إزاي تنتقل.

مرسلات للمطوّرين

الـ API والويب هوك

ابعت كل أنواع الرسايل — نص وميديا وملصقات ومواقع وجهات اتصال واستطلاعات وأزرار وقوائم — من رقمك المربوط بـ endpoint واحد، وكل رد وصوت وتحديث تسليم وطلب إلغاء يوصل لسيرفرك — موقّع، وبيتعاد لو فشل، ومتسجّل.

  • REST + JSON
  • endpoint واحد لكل الأنواع
  • ويب هوك موقّع (HMAC-SHA256)
  • cURL · Node · PHP · Python
العنوان الأساسي
https://mursilat.com/api/v1
إصدار الويب هوك
2026-10
في الصفحة دي

البداية السريعة

مرسلات API بتخلّي نظامك يبعت رسائل من رقمك أنت المربوط — كل الأنواع، من النص والميديا لحد الاستطلاعات والأزرار والقوائم، من endpoint واحد — ويستقبل ردود العملاء وتحديثات التسليم على الويب هوك بتاعك.

  1. اربط رقمك من لوحة التحكم: الأرقام ← إضافة رقم، وامسح كود الـ QR.
  2. افتح الأرقام ← رقمك ← تبويب المطوّرين واعمل مفتاح API (بيظهر مرة واحدة) واختار صلاحياته.
  3. في نفس التبويب احفظ رابط الويب هوك، اختار الإصدار 2، وانسخ سرّ التوقيع (بيظهر مرة واحدة).
  4. ابعت أول رسالة أزرار (تحت). كل إرسال محتاج هيدر Idempotency-Key.
  5. العميل يضغط زرار — أو يكتب رقمه — ويوصلك حدث interactive.reply على الويب هوك.
curl -X POST "https://mursilat.com/api/v1/whatsapp/public/messages" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $WASLA_API_KEY" \
  -H "Idempotency-Key: order-1042-confirm" \
  -d '{
  "to": "201001234567",
  "type": "buttons",
  "body": {
    "text": "هل تؤكد طلبك؟"
  },
  "footer": {
    "text": "متجر النور"
  },
  "buttons": [
    {
      "id": "confirm",
      "title": "تأكيد"
    },
    {
      "id": "cancel_order",
      "title": "إلغاء الطلب"
    }
  ],
  "reference": "order-1042"
}'

العنوان الأساسي: https://mursilat.com/api/v1. الطلبات والردود JSON بترميز UTF-8، والأوقات ISO-8601 بتوقيت UTC. أرقام التليفون بالصيغة الدولية من غير + (مثلاً 201001234567).

المصادقة ومفاتيح API

كل رقم ليه مفتاح API خاص بيه. ابعته في هيدر x-api-key من السيرفر بتاعك (أبداً مش من المتصفح أو تطبيق الموبايل). إحنا بنحتفظ ببصمة المفتاح بس، فهو بيظهر مرة واحدة وقت ما تعمله؛ ولو عملت مفتاح جديد القديم بيبطل فوراً.

الصلاحية (scope)بتسمح بـ
sendبتسمح بـكل الإرسال على POST /messages، والتفاعل/التعديل/المسح، وGET /interactions/:id
mediaبتسمح بـأنواع الميديا (مع send)، ورفع الميديا وتحميلها
webhookبتسمح بـاستقبال أحداث الويب هوك

الوصول للـ API جزء من باقتك. من غيره أي طلب بيرجع 403 API_ACCESS_NOT_IN_PLAN.

منع التكرار (Idempotency)

كل إرسال لازم يكون معاه هيدر Idempotency-Key (لحد 191 حرف، مختلف لكل رسالة — مثلاً order-1042-confirm). لو الطلب عمل timeout، أعد المحاولة بنفس المفتاح: هيرجعلك نفس النتيجة الأولى والرسالة عمرها ما هتتبعت مرتين.

  • IDEMPOTENCY_KEY_REQUIRED — الهيدر مش موجود.
  • IDEMPOTENCY_KEY_REUSED — المفتاح اتستخدم قبل كده مع محتوى مختلف.
  • IDEMPOTENCY_IN_PROGRESS — أول طلب بالمفتاح ده لسه شغال؛ جرّب تاني بعد ثواني.

ابعت أي رسالة (endpoint موحّد)

POST /whatsapp/public/messages هو endpoint واحد لـ كل أنواع الرسايل: ابعت { "to": "…", "type": "…", …الخانات }. صلاحية send (وأنواع الميديا محتاجة كمان media)، وباقة فيها الـ API، وهيدر Idempotency-Key.

`type`هو إيهالتفاصيل
textهو إيهنص بتنسيق واتساب ومعاينة للرابط (اختياري)التفاصيلالنص
image · video · audio · voice · documentهو إيهصور، فيديو (GIF أو فيديو دائري)، ملفات صوت، رسايل صوتية، ملفاتالتفاصيلالميديا
stickerهو إيهملصق WebP — وPNG/JPG/GIF بنحولهمالتفاصيلالملصقات
locationهو إيهدبوس على الخريطة (مش موقع مباشر)التفاصيلالموقع
contactهو إيهمن 1 لـ 5 جهات اتصالالتفاصيلجهات الاتصال
pollهو إيهسؤال بـ 2 لـ 12 اختيار، والأصوات بترجعلك في poll.voteالتفاصيلالاستطلاعات
eventهو إيهدعوة لمناسبة — تجريبيالتفاصيلالمناسبات
buttons · list · cta_urlهو إيهأزرار رد، قائمة اختيارات، زرار رابط (مع بديل مرقّم)التفاصيلالأزرار

الخانات المشتركة

الخانةملاحظات
toملاحظاتمطلوب. أرقام دولية من غير + (مثال: 201001234567).
typeملاحظاتمطلوب. نوع من الأنواع اللي فوق. غيرها بيرجع 400 MESSAGE_TYPE_NOT_SUPPORTED ومعاه reason (شوف مش ممكن).
referenceملاحظاتاختياري، لحد 128. معرّف من عندك بيرجع في message.status وinteractive.reply وpoll.vote.
replyToملاحظاتاختياري. رد على رسالة: الـ messageId اللي رجع من الإرسال، أو رقم رسالة في صندوق مرسلات، في نفس المحادثة.
curl -X POST "https://mursilat.com/api/v1/whatsapp/public/messages" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $WASLA_API_KEY" \
  -H "Idempotency-Key: order-1042-shipped" \
  -d '{
  "to": "201001234567",
  "type": "text",
  "text": "أهلاً أ. منى، طلبك رقم 1042 اتشحن وهيوصلك بكرة.",
  "reference": "order-1042"
}'
الرد
{
  "messageId": "3EB0A1B2C3D4E5F60718",
  "status": "sent",
  "type": "text",
  "reference": "order-1042",
  "interactionId": null,
  "format": null,
  "fallbackIncluded": null,
  "estimatedDelayMs": null
}

وقت الضغط الإرسال ممكن يرجع "status": "queued" ومعاه estimatedDelayMs: رقم الرسالة محجوز والنتيجة النهائية بتوصلك في ويب هوك message.status. الأنواع التفاعلية بترجع كمان interactionId وformat وfallbackIncluded.

الميديا: رابط أو ملف مرفوع

  • "media": { "url": "https://…" } — رابط عام بننزّله مرة واحدة وبأمان (من غير عناوين داخلية ولا تحويلات، 20 ثانية، وفي حدود حجم النوع).
  • "media": { "id": "med_…" } — ملف رفعته الأول من endpoint الرفع (بيتخزن بشكل خاص 24 ساعة، لنفس الرقم).
  • أو ابعت multipart/form-data بنفس الخانات ومعاهم file (والكائنات المتداخلة كنص JSON).
  • بنعرف نوع الملف الحقيقي من محتواه، مش من اسمه: لو مش مناسب للنوع بيرجع 415 MESSAGE_MEDIA_TYPE_NOT_ALLOWED.
ابعت ملف في نفس الطلب (multipart)
curl -X POST "https://mursilat.com/api/v1/whatsapp/public/messages" \
  -H "x-api-key: $WASLA_API_KEY" \
  -H "Idempotency-Key: invoice-1042-doc" \
  -F "to=201001234567" \
  -F "type=document" \
  -F "fileName=invoice-1042.pdf" \
  -F "file=@./invoice-1042.pdf"

الـ endpoints القديمة لسه شغالة

message/send وmedia/send وinteractive/send بقوا قديمة (legacy): لسه مدعومة بنفس الطلب والرد بالظبط، وبتعدي على نفس طريق الإرسال. أي ربط جديد يستخدم POST /whatsapp/public/messages.

النص

"type": "text" — لحد 4096 حرف. تنسيق واتساب بيوصل زي ما هو: *عريض* _مائل_ ~مشطوب~ والخط الثابت بين ثلاث علامات backtick.

الخانةالحدودملاحظات
textالحدود1–4096ملاحظاتمطلوب.
linkPreviewالحدودbooleanملاحظاتالافتراضي true: بنعمل معاينة (عنوان ووصف وصورة) لأول رابط https بأمان وخلال 3 ثواني. لو الصفحة مقريتش، النص بيتبعت من غير معاينة.
mentionsالحدود—ملاحظاتفي الجروبات بس (400 MESSAGE_MENTIONS_GROUP_ONLY)، والإرسال للجروبات مش متاح لسه.
curl -X POST "https://mursilat.com/api/v1/whatsapp/public/messages" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $WASLA_API_KEY" \
  -H "Idempotency-Key: order-1042-shipped" \
  -d '{
  "to": "201001234567",
  "type": "text",
  "text": "أهلاً أ. منى، طلبك رقم 1042 اتشحن وهيوصلك بكرة.",
  "reference": "order-1042"
}'

على الموبايل: فقاعة رسالة عادية على كل إصدارات واتساب. معاينة الروابط بتظهر على كل التطبيقات الحديثة.

الصور والفيديو والصوت والرسايل الصوتية والمستندات

كل أنواع الميديا بتاخد media (رابط أو رقم ملف مرفوع — شوف الميديا: رابط أو ملف مرفوع).

`type`الصيغ والحجمخانات إضافية
imageالصيغ والحجمJPEG · PNG · WebP، لحد 5 ميجاخانات إضافيةcaption لحد 1024 · viewOnce
videoالصيغ والحجمMP4 · 3GPP، لحد 16 ميجاخانات إضافيةcaption لحد 1024 · viewOnce · gif · videoNote
audioالصيغ والحجمOGG · MP3 · M4A · AAC · AMR، لحد 16 ميجاخانات إضافية—
voiceالصيغ والحجمأي صيغة صوت من دول، لحد 16 ميجاخانات إضافيةviewOnce
documentالصيغ والحجمأي ملف ماعدا البرامج، لحد 100 ميجاخانات إضافيةfileName لحد 120 · caption لحد 1024

صورة

curl -X POST "https://mursilat.com/api/v1/whatsapp/public/messages" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $WASLA_API_KEY" \
  -H "Idempotency-Key: launch-photo-201001234567" \
  -d '{
  "to": "201001234567",
  "type": "image",
  "media": {
    "url": "https://example.com/products/new.jpg"
  },
  "caption": "المنتج الجديد وصل 🎉"
}'

على الموبايل: صورة بالتعليق بتاعها. مع viewOnce: true بتتفتح مرة واحدة وبعدها تختفي — العرض مرة واحدة محتاج نسخة واتساب حديثة، والتطبيقات القديمة ممكن متفتحهاش.

فيديو وGIF وفيديو دائري

curl -X POST "https://mursilat.com/api/v1/whatsapp/public/messages" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $WASLA_API_KEY" \
  -H "Idempotency-Key: howto-video-201001234567" \
  -d '{
  "to": "201001234567",
  "type": "video",
  "media": {
    "id": "med_01J9ZK4A2B3C4D5E6F7G8H9J0K"
  },
  "caption": "إزاي تستخدم المنتج في دقيقة"
}'
  • gif: true — بيتكرر من غير صوت، زي الـ GIF.
  • videoNote: true — فيديو دائري: أقصاه 60 ثانية (MESSAGE_VIDEO_NOTE_TOO_LONG)، من غير تعليق، ومينفعش يبقى عرض مرة واحدة ولا GIF. الفيديو المربع بيبان أحسن (إحنا مش بنعيد ترميزه). التطبيقات القديمة ممكن تعرضه كفيديو عادي.
curl -X POST "https://mursilat.com/api/v1/whatsapp/public/messages" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $WASLA_API_KEY" \
  -H "Idempotency-Key: thanks-note-201001234567" \
  -d '{
  "to": "201001234567",
  "type": "video",
  "media": {
    "id": "med_01J9ZK4A2B3C4D5E6F7G8H9J0L"
  },
  "videoNote": true
}'

ملف صوتي

curl -X POST "https://mursilat.com/api/v1/whatsapp/public/messages" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $WASLA_API_KEY" \
  -H "Idempotency-Key: jingle-201001234567" \
  -d '{
  "to": "201001234567",
  "type": "audio",
  "media": {
    "url": "https://example.com/audio/jingle.mp3"
  }
}'

على الموبايل: ملف صوتي بزرار تشغيل واسمه — مش رسالة صوتية.

رسالة صوتية

curl -X POST "https://mursilat.com/api/v1/whatsapp/public/messages" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $WASLA_API_KEY" \
  -H "Idempotency-Key: voice-reply-1042" \
  -d '{
  "to": "201001234567",
  "type": "voice",
  "media": {
    "id": "med_01J9ZK4A2B3C4D5E6F7G8H9J0M"
  },
  "reference": "order-1042"
}'

على الموبايل: رسالة صوتية زي اللي بتتسجل من التطبيق. OGG/Opus وWebM بيحافظوا على شكل الموجة؛ والصيغ التانية بنحولها لو ينفع، ولو لأ بتشتغل كرسالة صوتية من غير شكل الموجة.

مستند

curl -X POST "https://mursilat.com/api/v1/whatsapp/public/messages" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $WASLA_API_KEY" \
  -H "Idempotency-Key: order-1042-invoice" \
  -d '{
  "to": "201001234567",
  "type": "document",
  "media": {
    "url": "https://example.com/invoices/1042.pdf"
  },
  "fileName": "invoice-1042.pdf",
  "caption": "فاتورة طلبك رقم 1042",
  "reference": "order-1042"
}'

على الموبايل: كارت ملف باسمه وحجمه ونوعه. fileName افتراضياً اسم الملف المرفوع أو آخر جزء في الرابط.

الملصقات

"type": "sticker" مع media.

  • WebP (ثابت أو متحرك) لحد 500 كيلوبايت بيتبعت زي ما هو.
  • PNG وJPG وGIF (لحد 5 ميجا) بنحولهم لملصق WebP مقاس 512×512 بخلفية شفافة، والـ GIF المتحرك بيبقى ملصق متحرك.
  • أي حاجة تانية بترجع MESSAGE_STICKER_INVALID.
curl -X POST "https://mursilat.com/api/v1/whatsapp/public/messages" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $WASLA_API_KEY" \
  -H "Idempotency-Key: thanks-sticker-1042" \
  -d '{
  "to": "201001234567",
  "type": "sticker",
  "media": {
    "url": "https://example.com/stickers/thanks.webp"
  }
}'

على الموبايل: ملصق من غير فقاعة، على كل إصدارات واتساب الحديثة.

الموقع

"type": "location" — دبوس ثابت. الموقع المباشر مينفعش يتبعت من رقم مربوط.

الخانةالحدود
latitudeالحدودمن −90 لـ 90 (مطلوب)
longitudeالحدودمن −180 لـ 180 (مطلوب)
nameالحدودلحد 100
addressالحدودلحد 200
curl -X POST "https://mursilat.com/api/v1/whatsapp/public/messages" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $WASLA_API_KEY" \
  -H "Idempotency-Key: store-location-1042" \
  -d '{
  "to": "201001234567",
  "type": "location",
  "latitude": 30.0444,
  "longitude": 31.2357,
  "name": "متجر النور",
  "address": "12 شارع التحرير، القاهرة"
}'

على الموبايل: معاينة خريطة بالاسم والعنوان، ولما العميل يدوس عليها بتفتح تطبيق الخرائط.

جهات الاتصال

"type": "contact" — من 1 لـ 5 كروت. الـ vCard بنبنيه على السيرفر (متأمّن، ولكل رقم رابط واتساب).

الخانةالحدود
contacts[]الحدود1–5
contacts[].nameالحدودلحد 100 (مطلوب)
contacts[].phones[]الحدود1–3، أرقام بكود الدولة (عادي + والمسافات)
contacts[].emailالحدودلحد 120، إيميل صحيح
contacts[].organizationالحدودلحد 100
curl -X POST "https://mursilat.com/api/v1/whatsapp/public/messages" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $WASLA_API_KEY" \
  -H "Idempotency-Key: support-card-1042" \
  -d '{
  "to": "201001234567",
  "type": "contact",
  "contacts": [
    {
      "name": "خدمة عملاء متجر النور",
      "phones": [
        "+20 100 302 2759"
      ],
      "email": "support@example.com",
      "organization": "متجر النور"
    }
  ]
}'

على الموبايل: كارت جهة اتصال بزرارين «مراسلة» و«إضافة»؛ والكروت المتعددة بتظهر في فقاعة واحدة.

الاستطلاعات

"type": "poll" — سؤال بـ 2 لـ 12 اختيار مختلفين. والأصوات بترجعلك.

الخانةالحدود
nameالحدودالسؤال، لحد 255 (مطلوب)
options[]الحدود2–12، مختلفين، كل واحد لحد 100
selectableCountالحدود1 (الافتراضي) = إجابة واحدة · لحد عدد الاختيارات · 0 = أي عدد إجابات
curl -X POST "https://mursilat.com/api/v1/whatsapp/public/messages" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $WASLA_API_KEY" \
  -H "Idempotency-Key: survey-7-201001234567" \
  -d '{
  "to": "201001234567",
  "type": "poll",
  "name": "أنهي يوم يناسبك للتوصيل؟",
  "options": [
    "الأحد",
    "الاثنين",
    "الثلاثاء"
  ],
  "selectableCount": 1,
  "reference": "survey-7"
}'

الأصوات (poll.vote)

poll.vote

حدث كل مرة حد يصوّت أو يغيّر صوته، للاستطلاعات اللي رقمك بعتها. selectedOptions هو اختياره الحالي ([] = شال صوته). وreference هو اللي انت بعته. الأصوات بتظهر كمان في صندوق رسايل مرسلات.

poll.vote — مثال
{
  "id": "evt_01J9ZK3Q8V6Y2M4N7P1R5T0W9H",
  "type": "poll.vote",
  "apiVersion": "2026-10",
  "createdAt": "2026-10-10T12:04:00.000Z",
  "sessionId": "cm1abc2def3ghi4jkl5mno6p",
  "number": "201100302759",
  "data": {
    "messageId": "3EB0A1B2C3D4E5F60718",
    "reference": "survey-7",
    "from": {
      "phone": "201001234567",
      "lid": null,
      "name": "منى علي"
    },
    "selectedOptions": [
      "الأحد"
    ],
    "timestamp": "2026-10-10T12:04:00.000Z"
  }
}

على الموبايل: استطلاع واتساب الأصلي على كل الإصدارات الحديثة.

المناسبات (تجريبي)

تجريبي

موبايلات كتير بتعرض المناسبات في الجروبات والمجتمعات بس، فالمناسبة في محادثة فردية ممكن تظهر «رسالة غير مدعومة» على بعض الموبايلات. جرّب على موبايلك الأول.

الخانةالحدود
nameالحدودلحد 100 (مطلوب)
descriptionالحدودلحد 2048
startAtالحدودتاريخ ISO في المستقبل، لحد سنة قدام (مطلوب)
endAtالحدودتاريخ ISO بعد startAt
locationNameالحدودلحد 100
extraGuestsAllowedالحدودboolean
curl -X POST "https://mursilat.com/api/v1/whatsapp/public/messages" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $WASLA_API_KEY" \
  -H "Idempotency-Key: opening-201001234567" \
  -d '{
  "to": "201001234567",
  "type": "event",
  "name": "افتتاح فرعنا الجديد",
  "description": "خصم 20% لأول 50 عميل",
  "startAt": "2026-11-01T17:00:00.000Z",
  "endAt": "2026-11-01T21:00:00.000Z",
  "locationName": "متجر النور — مدينة نصر"
}'

إرسال أزرار

POST /whatsapp/public/messages مع "type": "buttons" — لحد 3 أزرار رد. صلاحية send. والـ endpoint القديم POST /whatsapp/public/interactive/send بياخد نفس الطلب بالظبط ولسه شغال.

اقرا ده الأول: الأزرار على رقم مربوط

مرسلات بتبعت من رقمك المربوط (زي واتساب ويب)، مش من Meta Cloud API. على بعض الموبايلات وإصدارات التطبيق الأزرار ممكن تظهر كنص عادي من غير أزرار. عشان كده كل رسالة تفاعلية بيتضاف لها افتراضياً بديل مرقّم: العميل يقدر دايماً يرد بكتابة الرقم (1 أو 2 أو 3 — والأرقام العربية ١ ٢ ٣ بتشتغل) أو اسم الاختيار. وبأي طريقة رد، بيوصلك نفس حدث interactive.reply.

الخانةالحدودملاحظات
toالحدود—ملاحظاتمطلوبة. الرقم بالصيغة الدولية.
typeالحدودbuttons · list · cta_urlملاحظاتمطلوبة.
header.textالحدودلحد 60ملاحظاتاختيارية.
body.textالحدود1–1024ملاحظاتمطلوبة.
footer.textالحدودلحد 60ملاحظاتاختيارية.
buttons[]الحدود1–3ملاحظاتid لحد 64 (A-Z a-z 0-9 _ . : -، مختلف لكل زرار) · title لحد 20 (مختلف لكل زرار).
fallbackالحدودnumbered · noneملاحظاتالافتراضي numbered.
localeالحدودar · enملاحظاتلغة سطر التعليمات تحت الاختيارات. الافتراضي ar.
referenceالحدودلحد 128ملاحظاتمعرّف من عندك بيرجع في الويب هوك.
expiresInMinutesالحدود5–10080ملاحظاتمدة قبول الرد. الافتراضي 1440 (24 ساعة).
curl -X POST "https://mursilat.com/api/v1/whatsapp/public/messages" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $WASLA_API_KEY" \
  -H "Idempotency-Key: order-1042-confirm" \
  -d '{
  "to": "201001234567",
  "type": "buttons",
  "body": {
    "text": "هل تؤكد طلبك؟"
  },
  "footer": {
    "text": "متجر النور"
  },
  "buttons": [
    {
      "id": "confirm",
      "title": "تأكيد"
    },
    {
      "id": "cancel_order",
      "title": "إلغاء الطلب"
    }
  ],
  "reference": "order-1042"
}'
الرد
{
  "messageId": "3EB0A1B2C3D4E5F60718",
  "interactionId": "int_8f2c1d9a7b3e4f56a1b2c3d4",
  "status": "sent",
  "format": "native_flow_quick_reply",
  "fallbackIncluded": true
}

العميل بيشوف إيه لو الأزرار مظهرتش

مع fallback: "numbered" الاختيارات بتتضاف تحت نص الرسالة، وبعدها سطر توضيح (بلغة حسابك):

نص الرسالة
هل تؤكد طلبك؟

1) تأكيد
2) إلغاء الطلب

اكتب رقم اختيارك

fallback: "none" بيبعت النص بس. مش بننصح بيه: العميل اللي موبايله مش بيعرض الأزرار مش هيشوف الاختيارات (لو كتب اسم الاختيار بالظبط هيتطابق برضه).

إرسال قائمة

"type": "list" — زرار قائمة بيفتح لحد 10 اختيارات متقسّمة أقسام. كل اختيار ممكن يكون له وصف.

الخانةالحدود
list.buttonTextالحدودلحد 20 — اسم زرار القائمة
list.sections[]الحدود1–10 أقسام، title لحد 24 (اختياري)
rows[]الحدود1–10 اختيارات إجمالاً · id لحد 64 ومختلف · title لحد 24 · description لحد 72 (اختياري)
curl -X POST "https://mursilat.com/api/v1/whatsapp/public/messages" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $WASLA_API_KEY" \
  -H "Idempotency-Key: menu-201001234567-1" \
  -d '{
  "to": "201001234567",
  "type": "list",
  "body": {
    "text": "إزاي نقدر نساعدك؟"
  },
  "list": {
    "buttonText": "اختار",
    "sections": [
      {
        "title": "الخدمات",
        "rows": [
          {
            "id": "prices",
            "title": "الأسعار",
            "description": "اعرف أسعارنا"
          },
          {
            "id": "hours",
            "title": "مواعيد العمل"
          }
        ]
      },
      {
        "title": "الدعم",
        "rows": [
          {
            "id": "agent",
            "title": "كلّم موظف",
            "description": "حد من الفريق هيرد عليك"
          }
        ]
      }
    ]
  },
  "reference": "menu-main"
}'

البديل المرقّم بيرقّم الاختيارات بالترتيب عبر كل الأقسام (1…n)، فالرقم اللي العميل يكتبه دايماً بيتطابق.

زرار رابط (CTA)

"type": "cta_url" — زرار واحد بيفتح رابط. مفيش رد متوقع، فمفيش interactive.reply ليه.

الخانةالحدود
cta.titleالحدودلحد 20
cta.urlالحدودرابط https:// عام
curl -X POST "https://mursilat.com/api/v1/whatsapp/public/messages" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $WASLA_API_KEY" \
  -H "Idempotency-Key: order-1042-track" \
  -d '{
  "to": "201001234567",
  "type": "cta_url",
  "body": {
    "text": "شحنتك في الطريق 🚚"
  },
  "cta": {
    "title": "تتبع الشحنة",
    "url": "https://example.com/track/1042"
  },
  "reference": "order-1042"
}'

البديل بيضيف الرابط في آخر الرسالة (🔗 https://…). وقواعد الروابط بتطبّق عليه: الروابط المختصرة ممنوعة، والأرقام اللي عمرها أقل من 7 أيام مش بتبعت روابط لجهات اتصال جديدة (شوف الحدود وحماية الرقم).

حاجات مش ممكنة من رقم مربوط

الأنواع دي بترجع 400 MESSAGE_TYPE_NOT_SUPPORTED ومعاها details.type وdetails.reason. دي مميزات في تطبيق الموبايل نفسه أو في الـ Cloud API الرسمي، مش في رقم مربوط.

`type``reason`ليه
live_location`reason`LINKED_DEVICE_LIMITATIONليهالموقع المباشر بيتشارك من الموبايل نفسه بس. ابعت دبوس location بداله.
payment`reason`NOT_AVAILABLEليهواتساب باي مش متاح في مصر ولا السعودية، ولا على الأجهزة المربوطة.
product · catalog · order`reason`BUSINESS_CATALOG_ONLYليهرسايل الكتالوج محتاجة كتالوج واتساب بيزنس على الموبايل. ابعت صورة + زرار رابط بدالها.
status · story`reason`OUT_OF_SCOPEليهالحالات مش رسايل محادثة.
call`reason`LINKED_DEVICE_LIMITATIONليهالمكالمات مينفعش تتعمل من جهاز مربوط.
template`reason`CLOUD_API_ONLYليهقوالب الرسايل موجودة في الـ Cloud API الرسمي بس.
album`reason`SEND_ITEMS_SEPARATELYليهابعت كل صورة أو فيديو في رسالة لوحدها.
أي حاجة تانية`reason`UNKNOWN_TYPEليهمش نوع رسالة.

رفع الميديا

POST /whatsapp/public/media/upload — صلاحية media، وmultipart/form-data فيه file (لحد 100 ميجا؛ وحد النوع بيتطبق وقت الإرسال). ارفع مرة واحدة وبعدها ابعت "media": { "id": "…" } — مفيد للملفات الكبيرة أو لما تبعت نفس الملف لعملاء كتير.

ارفع ملف
curl -X POST "https://mursilat.com/api/v1/whatsapp/public/media/upload" \
  -H "x-api-key: $WASLA_API_KEY" \
  -F "file=@./photo.jpg"

# → { "mediaId": "med_01J9ZK4A2B3C4D5E6F7G8H9J0K", "mimeType": "image/jpeg", "size": 184233,
#     "fileName": "photo.jpg", "expiresAt": "2026-10-11T12:00:00.000Z" }
خانة الردملاحظات
mediaIdملاحظاتاستخدمه في media.id (لنفس الرقم بس).
mimeType · size · fileNameملاحظاتاللي اكتشفناه من الملف.
expiresAtملاحظاتبعد 24 ساعة من الرفع. بعدها 410 MESSAGE_MEDIA_EXPIRED — ارفعه تاني.

الملفات المرفوعة خاصة (عمرها ما بتبقى روابط عامة) وبتتمسح لوحدها بعد 24 ساعة. Idempotency-Key اختياري هنا.

التفاعل والتعديل والمسح

POST /whatsapp/public/messages/:messageId/react | edit | delete — :messageId هو الرقم اللي رجع من الإرسال (أو رقم رسالة في صندوق مرسلات) على رقم المفتاح بتاعك. صلاحية send، وIdempotency-Key مطلوب. الأفعال دي مش بتبعت message.status.

تفاعل

curl -X POST "https://mursilat.com/api/v1/whatsapp/public/messages/3EB0A1B2C3D4E5F60718/react" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $WASLA_API_KEY" \
  -H "Idempotency-Key: react-3EB0A1B2-1" \
  -d '{
  "emoji": "👍"
}'

إيموجي واحد (و"" بيشيل تفاعلك). غير كده MESSAGE_REACTION_INVALID. الرد: { messageId, action: "react", emoji }.

تعديل

curl -X POST "https://mursilat.com/api/v1/whatsapp/public/messages/3EB0A1B2C3D4E5F60718/edit" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $WASLA_API_KEY" \
  -H "Idempotency-Key: edit-3EB0A1B2-1" \
  -d '{
  "text": "أهلاً أ. منى، طلبك رقم 1042 اتشحن وهيوصلك بعد بكرة."
}'

الرسايل النصية اللي رقمك بعتها من الـ API أو مرسلات بس، وخلال 15 دقيقة (409 MESSAGE_EDIT_WINDOW_PASSED و409 MESSAGE_NOT_EDITABLE). النص الجديد بيعدي على نفس قواعد المحتوى. العميل بيشوف «تم التعديل».

مسح

curl -X POST "https://mursilat.com/api/v1/whatsapp/public/messages/3EB0A1B2C3D4E5F60718/delete" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $WASLA_API_KEY" \
  -H "Idempotency-Key: delete-3EB0A1B2-1" \
  -d '{
  "forEveryone": true
}'

forEveryone: true بيمسح رسالتك عند الكل خلال 48 ساعة (409 MESSAGE_DELETE_WINDOW_PASSED و409 MESSAGE_NOT_DELETABLE)؛ والعميل بيشوف «تم حذف هذه الرسالة». وfalse بيمسحها عندك بس (بتختفي من موبايلك ومن صندوق الرسايل).

الردود (interactive.reply)

لما العميل يرد على رسالة أزرار أو قائمة بيوصلك interactive.reply — سواء ضغط زرار، أو اختار من القائمة، أو كتب الرقم، أو كتب اسم الاختيار. reply.source بيقولك إزاي: button · list · number · title.

  • أول رد هو اللي بيتحسب. التفاعل بيبقى ANSWERED؛ وأي رد بعده بيوصل كـ message.received بس.
  • الردود المكتوبة بتتطابق مع آخر تفاعل مفتوح للعميل على الرقم ده: الأرقام 1…n (إنجليزي أو عربي ١ ٢ ٣) أو اسم الاختيار.
  • أي نص مش متطابق بيوصل كرسالة عادية message.received — إحنا مش بنخمّن.
  • بعد expiresInMinutes التفاعل بيبقى EXPIRED ومش بيتطابق تاني.
  • كلمات إيقاف الرسايل (زي «إلغاء» أو “stop”) بتتعامل كطلب إيقاف الأول، فأسماء الاختيارات ماينفعش تكون كلمات إيقاف، ولازم تكون مختلفة عن بعض.

interactive.reply

استخدم reference (أو interactionId) عشان توصل لطلبك، وreply.id للاختيار اللي العميل اختاره.

interactive.reply — مثال
{
  "id": "evt_01J9ZK3Q8V6Y2M4N7P1R5T0W9X",
  "type": "interactive.reply",
  "apiVersion": "2026-10",
  "createdAt": "2026-10-10T12:00:03.000Z",
  "sessionId": "cm1abc2def3ghi4jkl5mno6p",
  "number": "201100302759",
  "data": {
    "interactionId": "int_8f2c1d9a7b3e4f56a1b2c3d4",
    "messageId": "3EB0A1B2C3D4E5F60718",
    "reference": "order-1042",
    "from": {
      "phone": "201001234567",
      "lid": null,
      "name": "منى علي"
    },
    "reply": {
      "id": "confirm",
      "title": "تأكيد",
      "index": 1,
      "source": "button"
    },
    "replyMessageId": "3EB0C9D8E7F6A5B40312",
    "timestamp": "2026-10-10T12:00:03.000Z"
  }
}

الاستعلام عن تفاعل

GET /whatsapp/public/interactions/:interactionId — صلاحية send. مفيد كخطة بديلة لو ويب هوك فاتك.

curl -X GET "https://mursilat.com/api/v1/whatsapp/public/interactions/int_8f2c1d9a7b3e4f56a1b2c3d4" \
  -H "x-api-key: $WASLA_API_KEY"
الرد
{
  "interactionId": "int_8f2c1d9a7b3e4f56a1b2c3d4",
  "messageId": "3EB0A1B2C3D4E5F60718",
  "type": "buttons",
  "reference": "order-1042",
  "status": "ANSWERED",
  "options": [
    {
      "id": "confirm",
      "title": "تأكيد",
      "index": 1
    },
    {
      "id": "cancel_order",
      "title": "إلغاء الطلب",
      "index": 2
    }
  ],
  "answer": {
    "id": "confirm",
    "title": "تأكيد",
    "index": 1,
    "source": "number",
    "replyMessageId": "3EB0C9D8E7F6A5B40312",
    "answeredAt": "2026-10-10T12:00:03.000Z"
  },
  "expiresAt": "2026-10-11T12:00:00.000Z",
  "createdAt": "2026-10-10T12:00:00.000Z"
}

الويب هوك

الويب هوك بيبعت الأحداث لسيرفرك أول ما تحصل: رسايل واردة، تحديثات التسليم، ردود الأزرار، طلبات الإيقاف، وحالة الرقم. الإصدار 2 له شكل ثابت وموثّق لكل حدث.

الإعداد

  1. الأرقام ← رقمك ← تبويب المطوّرين: احفظ الرابط، اختار الإصدار 2، وحدّد الأحداث اللي عايزها.
  2. انسخ سرّ التوقيع (بيظهر مرة واحدة — وتقدر تعمل واحد جديد في أي وقت).
  3. اضغط إرسال حدث تجريبي وشوف النتيجة في سجل الويب هوك.
  • رابط عام https:// (أو http://) على بورت 80 أو 443. العناوين الداخلية والخاصة مرفوضة.
  • رد بأي 2xx خلال 10 ثواني — والشغل التقيل اعمله بعد ما ترد.
  • التحويلات (redirects) مش بنتبعها.
هيدرز الطلب
POST /webhooks/mursilat HTTP/1.1
Content-Type: application/json
User-Agent: Wasla-Webhooks/2
X-Wasla-Event: interactive.reply
X-Wasla-Delivery: dlv_01J9ZK3R2B7C8D9E0F1G2H3J4K
X-Wasla-Webhook-Version: 2
X-Wasla-Signature: t=1760097603,v1=3f6c2b5e9a0d4c7f8e1b2a3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f70
خانة الغلافالوصف
idالوصفمعرّف الحدث الفريد (evt_…). ثابت في كل إعادة محاولة وفي إعادة الإرسال اليدوي — اعتمد عليه لمنع التكرار.
typeالوصفنوع الحدث، مثلاً interactive.reply.
apiVersionالوصف2026-10
createdAtالوصفوقت حصول الحدث (ISO-8601 UTC). رتّب بيه الأحداث.
sessionId · numberالوصفالحدث يخص أنهي رقم من أرقامك.
dataالوصفمحتوى الحدث (تحت).

الأحداث

message.received

عميل بعت رسالة. type واحد من text · image · video · audio · voice · document · sticker · location · contact · reaction · interactive_reply · unsupported. عمرنا ما بنبعت كائن واتساب الخام.

message.received — مثال
{
  "id": "evt_01J9ZK3Q8V6Y2M4N7P1R5T0W9A",
  "type": "message.received",
  "apiVersion": "2026-10",
  "createdAt": "2026-10-10T12:00:00.000Z",
  "sessionId": "cm1abc2def3ghi4jkl5mno6p",
  "number": "201100302759",
  "data": {
    "messageId": "3EB0F1E2D3C4B5A69788",
    "from": {
      "phone": "201001234567",
      "lid": null,
      "name": "منى علي"
    },
    "chat": {
      "type": "direct"
    },
    "type": "text",
    "text": "ممكن أعرف ميعاد التوصيل؟",
    "caption": null,
    "media": null,
    "location": null,
    "replyTo": null,
    "timestamp": "2026-10-10T12:00:00.000Z"
  }
}

message.received(image)

رسايل الميديا فيها media.url: رابط تحميل موقّع صالح 24 ساعة. ورسالة الجروب فيها chat.type: "group".

message.received — مثال
{
  "id": "evt_01J9ZK3Q8V6Y2M4N7P1R5T0W9B",
  "type": "message.received",
  "apiVersion": "2026-10",
  "createdAt": "2026-10-10T12:01:00.000Z",
  "sessionId": "cm1abc2def3ghi4jkl5mno6p",
  "number": "201100302759",
  "data": {
    "messageId": "3EB0A9B8C7D6E5F40011",
    "from": {
      "phone": "201001234567",
      "lid": null,
      "name": "منى علي"
    },
    "chat": {
      "type": "group",
      "groupId": "120363025746123456@g.us",
      "groupName": "عملاء المتجر"
    },
    "type": "image",
    "text": null,
    "caption": "صورة الإيصال",
    "media": {
      "id": "cm2media0001",
      "mimeType": "image/jpeg",
      "size": 184233,
      "url": "https://mursilat.com/api/v1/whatsapp/public/media/cm2media0001?exp=1760184060&sig=5d41402abc4b2a76b9719d911017c592"
    },
    "location": null,
    "replyTo": {
      "messageId": "3EB0A1B2C3D4E5F60718"
    },
    "timestamp": "2026-10-10T12:01:00.000Z"
  }
}
تحميل الميديا
# Signed link from the webhook (valid 24h) — no API key needed:
curl -o receipt.jpg "https://mursilat.com/api/v1/whatsapp/public/media/cm2media0001?exp=1760184060&sig=5d41…"

# After it expires: use your API key (scope: media)
curl -o receipt.jpg -H "x-api-key: $WASLA_API_KEY" "https://mursilat.com/api/v1/whatsapp/public/media/cm2media0001"

message.status

تحديثات تسليم الرسايل اللي بعتها (من الـ API ولوحة التحكم): sent · delivered · read · failed (مع error). وreference هو اللي بعته.

message.status — مثال
{
  "id": "evt_01J9ZK3Q8V6Y2M4N7P1R5T0W9C",
  "type": "message.status",
  "apiVersion": "2026-10",
  "createdAt": "2026-10-10T12:00:02.000Z",
  "sessionId": "cm1abc2def3ghi4jkl5mno6p",
  "number": "201100302759",
  "data": {
    "messageId": "3EB0A1B2C3D4E5F60718",
    "reference": "order-1042",
    "to": "201001234567",
    "status": "delivered",
    "error": null,
    "timestamp": "2026-10-10T12:00:02.000Z"
  }
}

interactive.reply

العميل رد على الأزرار أو القائمة (شوف الردود).

interactive.reply — مثال
{
  "id": "evt_01J9ZK3Q8V6Y2M4N7P1R5T0W9X",
  "type": "interactive.reply",
  "apiVersion": "2026-10",
  "createdAt": "2026-10-10T12:00:03.000Z",
  "sessionId": "cm1abc2def3ghi4jkl5mno6p",
  "number": "201100302759",
  "data": {
    "interactionId": "int_8f2c1d9a7b3e4f56a1b2c3d4",
    "messageId": "3EB0A1B2C3D4E5F60718",
    "reference": "order-1042",
    "from": {
      "phone": "201001234567",
      "lid": null,
      "name": "منى علي"
    },
    "reply": {
      "id": "confirm",
      "title": "تأكيد",
      "index": 1,
      "source": "button"
    },
    "replyMessageId": "3EB0C9D8E7F6A5B40312",
    "timestamp": "2026-10-10T12:00:03.000Z"
  }
}

poll.vote

حد صوّت (أو غيّر صوته) في استطلاع رقمك بعته (شوف الاستطلاعات).

poll.vote — مثال
{
  "id": "evt_01J9ZK3Q8V6Y2M4N7P1R5T0W9H",
  "type": "poll.vote",
  "apiVersion": "2026-10",
  "createdAt": "2026-10-10T12:04:00.000Z",
  "sessionId": "cm1abc2def3ghi4jkl5mno6p",
  "number": "201100302759",
  "data": {
    "messageId": "3EB0A1B2C3D4E5F60718",
    "reference": "survey-7",
    "from": {
      "phone": "201001234567",
      "lid": null,
      "name": "منى علي"
    },
    "selectedOptions": [
      "الأحد"
    ],
    "timestamp": "2026-10-10T12:04:00.000Z"
  }
}

contact.opted_out

العميل طلب يوقف الرسايل (كلمة إيقاف). أي إرسال ليه بعد كده بيترفض بـ SAFETY_SUPPRESSED.

contact.opted_out — مثال
{
  "id": "evt_01J9ZK3Q8V6Y2M4N7P1R5T0W9D",
  "type": "contact.opted_out",
  "apiVersion": "2026-10",
  "createdAt": "2026-10-10T12:05:00.000Z",
  "sessionId": "cm1abc2def3ghi4jkl5mno6p",
  "number": "201100302759",
  "data": {
    "phone": "201001234567",
    "lid": null,
    "keyword": "إلغاء",
    "timestamp": "2026-10-10T12:05:00.000Z"
  }
}

contact.opted_in

العميل رجع يستقبل رسايل. keyword بيبقى null في التغيير اليدوي.

contact.opted_in — مثال
{
  "id": "evt_01J9ZK3Q8V6Y2M4N7P1R5T0W9E",
  "type": "contact.opted_in",
  "apiVersion": "2026-10",
  "createdAt": "2026-10-10T12:06:00.000Z",
  "sessionId": "cm1abc2def3ghi4jkl5mno6p",
  "number": "201100302759",
  "data": {
    "phone": "201001234567",
    "lid": null,
    "keyword": null,
    "timestamp": "2026-10-10T12:06:00.000Z"
  }
}

session.status

حالة اتصال رقمك اتغيّرت: connected · disconnected · banned · logged_out.

session.status — مثال
{
  "id": "evt_01J9ZK3Q8V6Y2M4N7P1R5T0W9F",
  "type": "session.status",
  "apiVersion": "2026-10",
  "createdAt": "2026-10-10T12:07:00.000Z",
  "sessionId": "cm1abc2def3ghi4jkl5mno6p",
  "number": "201100302759",
  "data": {
    "status": "disconnected",
    "reason": "connection_lost",
    "timestamp": "2026-10-10T12:07:00.000Z"
  }
}

webhook.test

بيتبعت من زرار إرسال حدث تجريبي. دايماً بغلاف الإصدار 2 وبيوصل حتى لو مش ضمن الأحداث اللي اخترتها.

webhook.test — مثال
{
  "id": "evt_01J9ZK3Q8V6Y2M4N7P1R5T0W9G",
  "type": "webhook.test",
  "apiVersion": "2026-10",
  "createdAt": "2026-10-10T12:08:00.000Z",
  "sessionId": "cm1abc2def3ghi4jkl5mno6p",
  "number": "201100302759",
  "data": {
    "message": "This is a test event from Mursilat",
    "timestamp": "2026-10-10T12:08:00.000Z"
  }
}

التحقق من التوقيع

كل طلب فيه هيدر X-Wasla-Signature: t=<unix>,v1=<hex> — t هو الوقت بالثواني (Unix)، وv1 هو HMAC-SHA256(secret, "<t>.<raw body>") بحروف hex صغيرة.

  1. اقرا جسم الطلب الخام (البايتات زي ما وصلت) — قبل أي تحويل لـ JSON.
  2. قسّم الهيدر على , و= عشان تطلع t وv1.
  3. ارفض الطلب لو t بعيد عن ساعتك أكتر من 5 دقايق (حماية من إعادة الإرسال).
  4. احسب HMAC-SHA256 لـ t + "." + rawBody بالسرّ بتاعك، بصيغة hex.
  5. قارنه بـ v1 بمقارنة ثابتة الوقت (constant-time). وبعدها بس حوّل الـ JSON.
// Node.js + Express. Use the RAW body: JSON.parse → JSON.stringify changes bytes and breaks the signature.
import crypto from 'node:crypto';
import express from 'express';

const app = express();
const SECRET = process.env.WASLA_WEBHOOK_SECRET;
const seen = new Set(); // use Redis/DB in production

function verifyWaslaSignature(rawBody, header, secret, toleranceSec = 300) {
  if (!header || !secret) return false;
  const parts = {};
  for (const pair of header.split(',')) {
    const i = pair.indexOf('=');
    if (i > 0) parts[pair.slice(0, i).trim()] = pair.slice(i + 1).trim();
  }
  const t = parts.t;
  const v1 = parts.v1;
  if (!/^\d+$/.test(t || '') || !/^[0-9a-f]{64}$/i.test(v1 || '')) return false;
  // Reject old (replayed) or far-future timestamps.
  if (Math.abs(Math.floor(Date.now() / 1000) - Number(t)) > toleranceSec) return false;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(t + '.')
    .update(rawBody) // the RAW bytes exactly as received, not re-serialized JSON
    .digest();
  const received = Buffer.from(v1, 'hex');
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

app.post('/webhooks/mursilat', express.raw({ type: 'application/json' }), (req, res) => {
  if (!verifyWaslaSignature(req.body, req.get('X-Wasla-Signature'), SECRET)) {
    return res.status(401).send('invalid signature');
  }
  const event = JSON.parse(req.body.toString('utf8'));
  res.sendStatus(200); // answer fast (within 10s); do the work after

  if (seen.has(event.id)) return; // retries reuse the same event id
  seen.add(event.id);
  if (event.type === 'interactive.reply') {
    console.log(event.data.reference, '→', event.data.reply.id);
  }
});

app.listen(3000);

أغلب مشاكل «التوقيع غلط» سببها التحقق من جسم اتعاد تحويله (JSON.stringify(req.body)) بدل البايتات الخام، أو middleware بيحوّل الـ JSON قبل التحقق.

إعادة المحاولة والترتيب ومنع التكرار

المحاولةإمتى
1إمتىفوراً
2إمتىبعد 10 ثواني
3إمتىبعد دقيقة
4إمتىبعد 5 دقايق
5إمتىبعد 15 دقيقة
6إمتىبعد 30 دقيقة (حوالي 51 دقيقة من الأولى)
  • 2xx = اتسلّم. أي 4xx/5xx تاني أو timeout أو خطأ اتصال بيتعاد.
  • 410 Gone = «بطّلوا تبعتوا»: الويب هوك بيتوقف فوراً وبنبلّغك. احفظ الرابط تاني من تبويب المطوّرين عشان يشتغل.
  • X-Wasla-Delivery ثابت في كل محاولات التسليم الواحد؛ وإعادة الإرسال اليدوي ليها واحد جديد (بنفس id الحدث).
  • لحد 4 طلبات في نفس الوقت لكل رابط، فالأحداث ممكن توصل بترتيب مختلف. رتّب بـ createdAt وخلّي المعالجة idempotent على id.

حدث الاختبار

زرار إرسال حدث تجريبي (تبويب المطوّرين) بيبعت webhook.test لرابطك. استخدمه تتأكد إن رابطك بيوصله وإن كود التوقيع سليم. الزرار بيبقى مقفول — والسبب مكتوب جنبه — لو مفيش رابط محفوظ أو الويب هوك متوقف.

سجل التسليم

سجل الويب هوك في تبويب المطوّرين بيحتفظ بكل تسليم 7 أيام: الحالة، المحاولة n/6، كود HTTP، المدة، وميعاد المحاولة الجاية. افتح أي تسليم تشوف المحتوى، وهيدرز الطلب (التوقيع مخفي)، وأول 2 KB من رد سيرفرك، وكل المحاولات — وتقدر تعيد إرساله.

الأخطاء

كل الأخطاء ليها شكل واحد. اعتمد في الكود على code (ثابت)؛ أما message فنص إنجليزي للبشر وممكن يتغيّر. details فيها الخانة، أو في أخطاء «جرّب بعدين» فيها retryAt / retryAfterSeconds.

شكل الخطأ
{
  "statusCode": 400,
  "code": "INTERACTIVE_TOO_MANY_BUTTONS",
  "message": [
    "buttons: at most 3 buttons are allowed"
  ],
  "error": "INTERACTIVE_TOO_MANY_BUTTONS",
  "details": {
    "field": "buttons"
  },
  "timestamp": "2026-10-10T12:00:00.000Z",
  "path": "/api/v1/whatsapp/public/interactive/send"
}
الكودHTTPالمعنى
INTERACTIVE_INVALID_TYPEHTTP400المعنىtype لازم يكون buttons أو list أو cta_url.
INTERACTIVE_BODY_REQUIREDHTTP400المعنىbody.text مش موجود.
INTERACTIVE_TEXT_TOO_LONGHTTP400المعنىالعنوان أو النص أو التذييل أطول من الحد.
INTERACTIVE_TITLE_TOO_LONGHTTP400المعنىاسم زرار / اختيار / قسم / رابط أطول من الحد.
INTERACTIVE_TOO_MANY_BUTTONSHTTP400المعنىأكتر من 3 أزرار.
INTERACTIVE_NO_OPTIONSHTTP400المعنىمفيش أزرار / اختيارات / رابط.
INTERACTIVE_TOO_MANY_ROWSHTTP400المعنىأكتر من 10 اختيارات في القائمة.
INTERACTIVE_TOO_MANY_SECTIONSHTTP400المعنىأكتر من 10 أقسام في القائمة.
INTERACTIVE_DUPLICATE_IDHTTP400المعنىاختيارين ليهم نفس الـ id.
INTERACTIVE_DUPLICATE_TITLEHTTP400المعنىاختيارين ليهم نفس الاسم (لازم الأسماء تختلف عشان الرد المكتوب يتطابق).
INTERACTIVE_INVALID_IDHTTP400المعنىid فيه حروف غير A-Z a-z 0-9 _ . : - أو أطول من الحد.
INTERACTIVE_INVALID_URLHTTP400المعنىرابط الزرار مش رابط https:// عام.
INTERACTIVE_INVALID_EXPIRYHTTP400المعنىexpiresInMinutes برّه 5–10080.
INTERACTIVE_INVALID_FALLBACKHTTP400المعنىfallback لازم يكون numbered أو none.
INTERACTIVE_INVALID_LOCALEHTTP400المعنىlocale لازم يكون ar أو en.
INTERACTIVE_TITLE_REQUIREDHTTP400المعنىاسم زرار / اختيار / قسم / رابط مش موجود.
INTERACTIVE_REFERENCE_TOO_LONGHTTP400المعنىreference أطول من 128 حرف.
INTERACTIVE_RESERVED_TITLEHTTP400المعنىاسم اختيار هو كلمة إيقاف (زي «إلغاء»).
INTERACTIVE_UNEXPECTED_FIELDHTTP400المعنىخانة مش مناسبة للنوع ده (مثلاً buttons في قائمة).
MESSAGE_TYPE_REQUIRED · MESSAGE_TYPE_NOT_SUPPORTEDHTTP400المعنىtype ناقص، أو مينفعش يتبعت من رقم مربوط (details.reason، شوف مش ممكن).
MESSAGE_FIELD_REQUIRED · MESSAGE_FIELD_INVALIDHTTP400المعنىخانة مطلوبة ناقصة / قيمتها مش صحيحة (details.field وdetails.reason).
MESSAGE_TEXT_TOO_LONGHTTP400المعنىخانة نص أطول من حدها (details.limit).
MESSAGE_UNEXPECTED_FIELDHTTP400المعنىخانة تبع نوع تاني، زي caption مع audio أو مع الفيديو الدائري.
MESSAGE_MENTIONS_GROUP_ONLYHTTP400المعنىالمنشن في الجروبات بس.
MESSAGE_MEDIA_REQUIREDHTTP400المعنىمفيش media (أو file) لنوع ميديا.
MESSAGE_MEDIA_NOT_FOUNDHTTP404المعنىmedia.id مش معروف (أو اترفع على رقم تاني).
MESSAGE_MEDIA_EXPIREDHTTP410المعنىالملف المرفوع عدّى عليه 24 ساعة — ارفعه تاني.
MESSAGE_MEDIA_TOO_LARGEHTTP413المعنىأكبر من حد حجم النوع (details.limitBytes).
MESSAGE_MEDIA_TYPE_NOT_ALLOWEDHTTP415المعنىنوع الملف الحقيقي مش مناسب لنوع الرسالة (details.detected).
MESSAGE_MEDIA_FETCH_FAILEDHTTP422المعنىمقدرناش ننزّل رابط الميديا بأمان (details.reason).
MESSAGE_STICKER_INVALIDHTTP400المعنىالصورة مينفعش تتحول لملصق.
MESSAGE_VIDEO_NOTE_TOO_LONGHTTP400المعنىالفيديو الدائري أقصاه 60 ثانية.
MESSAGE_POLL_INVALIDHTTP400المعنىdetails.reason: QUESTION_REQUIRED · OPTIONS_COUNT · OPTIONS_DUPLICATE · OPTION_TOO_LONG · SELECTABLE_COUNT.
MESSAGE_CONTACT_INVALIDHTTP400المعنىكارت جهة اتصال مش صحيح (details.reason وdetails.index).
MESSAGE_LOCATION_INVALIDHTTP400المعنىخط العرض أو الطول خارج النطاق.
MESSAGE_EVENT_INVALIDHTTP400المعنىاسم المناسبة أو مواعيدها مش صحيحة (details.reason).
MESSAGE_REPLY_TARGET_NOT_FOUNDHTTP404المعنىreplyTo مش رسالة في المحادثة دي.
MESSAGE_NOT_FOUNDHTTP404المعنىتفاعل/تعديل/مسح: رسالة مش معروفة على الرقم ده.
MESSAGE_REACTION_INVALIDHTTP400المعنىاستخدم إيموجي واحد بالظبط (أو "" عشان تشيله).
MESSAGE_NOT_EDITABLE · MESSAGE_EDIT_WINDOW_PASSEDHTTP409المعنىرسايلك النصية بس، وخلال 15 دقيقة.
MESSAGE_NOT_DELETABLE · MESSAGE_DELETE_WINDOW_PASSEDHTTP409المعنىالمسح عند الكل: رسايلك بس، وخلال 48 ساعة.
INVALID_RECIPIENTHTTP400المعنىرقم التليفون مش صالح.
API_KEY_REQUIRED · API_KEY_INVALIDHTTP401المعنىx-api-key مش موجود أو غلط.
API_ACCESS_NOT_IN_PLANHTTP403المعنىباقتك مش فيها وصول للـ API.
MESSAGES_QUOTA_EXCEEDED · NO_ACTIVE_PLANHTTP403المعنىرسايل باقتك خلصت أو مفيش باقة فعّالة.
API_SCOPE_DISABLEDHTTP403المعنىالمفتاح مش معاه الصلاحية المطلوبة.
IDEMPOTENCY_KEY_REQUIREDHTTP409المعنىابعت هيدر Idempotency-Key.
IDEMPOTENCY_KEY_REUSEDHTTP409المعنىالمفتاح اتستخدم قبل كده مع محتوى مختلف.
IDEMPOTENCY_IN_PROGRESSHTTP409المعنىأول طلب بالمفتاح ده لسه شغال.
SESSION_NOT_CONNECTEDHTTP400المعنىالرقم مش متصل — اربطه تاني من لوحة التحكم.
API_TEMPORARY_BAN · TOO_MANY_REQUESTSHTTP429المعنىتخطيت حد الطلبات أو فيه إيقاف مؤقت (شوف retryAfterSeconds).
SAFETY_DAILY_CAP_REACHED · SAFETY_HOURLY_CAP_REACHEDHTTP429المعنىوصلت لحد حماية الرقم لجهات الاتصال الجديدة؛ جرّب تاني في retryAt.
SAFETY_PACING · SAFETY_NUMBER_BUSYHTTP429المعنىبتبعت بسرعة من الرقم ده؛ جرّب تاني في retryAt.
SAFETY_CONTENT_BLOCKEDHTTP422المعنىقاعدة محتوى: رابط مختصر، أو رابط من رقم جديد لجهة اتصال جديدة، أو نفس النص لعدد كبير من جهات اتصال جديدة (details.reasons).
SAFETY_SUPPRESSEDHTTP422المعنىالعميل طلب إيقاف الرسايل أو موجود في قائمة الاستبعاد.
SAFETY_NUMBER_PAUSEDHTTP423المعنىالرقم متوقف مؤقتاً لحمايته؛ الرد على المحادثات الحديثة لسه شغال.
SAFETY_NUMBER_BANNEDHTTP403المعنىواتساب قيّد الرقم ده.

الحدود وحماية الرقم

الحدود دي موجودة عشان تقلّل خطر إن واتساب يقيّد رقمك. مفيش أداة تقدر تضمن إن الرقم عمره ما يتحظر — وأهم حاجة إنك تراسل ناس مستنية رسايلك.

حدود الطلبات (الافتراضية)

الحدالافتراضي
إرسال نص / رسائل تفاعليةالافتراضي60 في الدقيقة لكل مفتاح ولكل رقم
إرسال ميدياالافتراضي30 في الدقيقة لكل مفتاح ولكل رقم
الدفعات السريعةالافتراضيأكتر من 3 رسايل في 5 ثواني بتدخل طابور (queued: true، تأخير 2–30 ثانية)
الحماية من الإساءةالافتراضيأكتر من 8 رسايل في 5 ثواني ← إيقاف مؤقت (429): 10 دقايق، بعدها ساعة، بعدها 24 ساعة

درع الرقم (Number Shield)

  • رسالة واحدة في نفس الوقت لكل رقم. الرسايل لـجهات اتصال جديدة بتتبعت بفواصل (حوالي 8–25 ثانية مع استراحات قصيرة)؛ والرد على عملاء كتبولك في آخر 24 ساعة بيطلع في 1–3 ثواني.
  • الأرقام الجديدة بتتسخّن تدريجياً: عدد جهات الاتصال الجديدة في اليوم بيزيد مع عمر الرقم (الجدول تحت). الرد على المحادثات الحديثة مش بيتحسب.
  • لو وصلت لحد، الـ API بيرد بكود SAFETY_* ومعاه retryAt — جرّب في الوقت ده، متعملش loop.
  • لو واتساب بيّن إنه بيحدّ الرقم، الرقم بيتوقف شوية (SAFETY_NUMBER_PAUSED)؛ والرد على المحادثات الحديثة بيفضل شغال.
عمر الرقمجهات جديدة / يومجهات جديدة / ساعة
الأيام 1–3جهات جديدة / يوم20جهات جديدة / ساعة10
الأيام 4–7جهات جديدة / يوم50جهات جديدة / ساعة15
الأسبوع التانيجهات جديدة / يوم100جهات جديدة / ساعة25
الأسابيع 3–4جهات جديدة / يوم200جهات جديدة / ساعة40
أكتر من 30 يوم (رقم سليم)جهات جديدة / يوم400جهات جديدة / ساعة60
  • الروابط المختصرة (bit.ly …) ممنوعة — استخدم الدومين بتاعك.
  • الأرقام اللي عمرها أقل من 7 أيام مش بتبعت روابط لجهات اتصال جديدة.
  • نفس النص لأكتر من 50 جهة اتصال جديدة في 24 ساعة بيتمنع — خصّص الرسالة لكل عميل.
  • العملاء اللي طلبوا إيقاف الرسايل مش بيوصلهم حاجة (SAFETY_SUPPRESSED).

نصيحة: الأزرار طريقة كويسة تبدأ بيها محادثة — لما العميل يرد، المحادثة بتبقى «حديثة» والردود بعد كده أسرع وخطرها أقل.

إرسال رسالة نصية (قديم)

endpoint قديم (legacy)

لسه مدعوم ومتغيّرش. للربط الجديد: POST /whatsapp/public/messages مع "type": "text" (النص).

POST /whatsapp/public/message/send — صلاحية send.

الخانةالنوعملاحظات
phoneNumberالنوعstringملاحظاتمطلوبة. الرقم بالصيغة الدولية من غير +.
contentالنوعstringملاحظاتمطلوبة. نص الرسالة.
referenceالنوعstringملاحظاتاختيارية، لحد 128. معرّف من عندك بيرجعلك في message.status.
curl -X POST "https://mursilat.com/api/v1/whatsapp/public/message/send" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $WASLA_API_KEY" \
  -H "Idempotency-Key: order-1042-shipped" \
  -d '{
  "phoneNumber": "201001234567",
  "content": "أهلاً أ. منى، طلبك رقم 1042 اتشحن وهيوصلك بكرة.",
  "reference": "order-1042"
}'
الرد
{
  "success": true,
  "message": "Message sent successfully",
  "sessionId": "cm1abc2def3ghi4jkl5mno6p",
  "phoneNumber": "201001234567",
  "messageId": "3EB0A1B2C3D4E5F60718"
}

لو بعتّ رسايل كتير ورا بعض، الرسالة ممكن تدخل طابور بدل ما تتبعت فوراً: الرد ساعتها فيه queued: true وestimatedDelayMs. النتيجة النهائية بتوصلك في ويب هوك message.status.

إرسال ميديا (قديم)

endpoint قديم (legacy)

لسه مدعوم ومتغيّرش. للربط الجديد: POST /whatsapp/public/messages مع نوع ميديا (الميديا)، واللي بيضيف الملصقات والرسايل الصوتية والعرض مرة واحدة والـ GIF والفيديو الدائري.

POST /whatsapp/public/media/send — صلاحية media. ابعت JSON فيه mediaUrl عام، أو multipart/form-data فيه file.

الخانةالنوعملاحظات
phoneNumberالنوعstringملاحظاتمطلوبة.
mediaTypeالنوعstringملاحظاتimage · video · document · audio
mediaUrlالنوعstringملاحظاترابط https:// عام (أو ارفع file بداله).
captionالنوعstringملاحظاتاختيارية.
filenameالنوعstringملاحظاتاختيارية، للمستندات.
voiceNoteالنوعbooleanملاحظاتاختيارية. يبعت الصوت كرسالة صوتية.
referenceالنوعstringملاحظاتاختيارية، لحد 128.
curl -X POST "https://mursilat.com/api/v1/whatsapp/public/media/send" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $WASLA_API_KEY" \
  -H "Idempotency-Key: order-1042-invoice" \
  -d '{
  "phoneNumber": "201001234567",
  "mediaType": "document",
  "mediaUrl": "https://example.com/invoices/1042.pdf",
  "filename": "invoice-1042.pdf",
  "caption": "فاتورة طلبك رقم 1042",
  "reference": "order-1042"
}'
رفع ملف (multipart)
curl -X POST "https://mursilat.com/api/v1/whatsapp/public/media/send" \
  -H "x-api-key: $WASLA_API_KEY" \
  -H "Idempotency-Key: invoice-1042" \
  -F "phoneNumber=201001234567" \
  -F "mediaType=document" \
  -F "filename=invoice-1042.pdf" \
  -F "file=@./invoice-1042.pdf"

روابط الميديا لازم تكون عامة. العناوين الداخلية والخاصة (localhost و10.x و192.168.x …) مرفوضة.

الانتقال من v1 إلى v2

الإصدار 1 من الويب هوك قديم ومُهمَل (deprecated) وهيتشال بعدين. الأرقام الموجودة بتفضل على v1 لحد ما تغيّرها بنفسك؛ والأرقام الجديدة بتبدأ على v2. هيدر التوقيع واحد في الاتنين.

v1 (قديم)v2
الأحداثv1 (قديم)message.received بسv2كل الأحداث، وانت تختار
المحتوىv1 (قديم)الشكل القديم برسالة واتساب الخام (rawMessage)v2غلاف ثابت + data محددة
User-Agentv1 (قديم)Wasla-Webhooks/1.0v2Wasla-Webhooks/2
هيدر الإصدارv1 (قديم)—v2X-Wasla-Webhook-Version: 2
إعادة المحاولة والسجلv1 (قديم)—v26 محاولات على حوالي 51 دقيقة، سجل 7 أيام، إعادة إرسال
التوقيعv1 (قديم)X-Wasla-Signature (نفسه)v2X-Wasla-Signature (نفسه)
  1. خلّي المستقبل عندك يقبل الاتنين: افصل حسب هيدر X-Wasla-Webhook-Version (لو مش موجود يبقى v1).
  2. اقرا message.received من data (from وtype وtext وmedia …) بدل الرسالة الخام.
  3. من تبويب المطوّرين غيّر لـ الإصدار 2، اختار الأحداث، واضغط إرسال حدث تجريبي.
  4. تابع سجل الويب هوك يوم، وبعدها شيل كود v1.

أسئلة شائعة

محتاج توثيق Meta للبيزنس أو Cloud API؟

لأ. مرسلات بتشتغل برقمك أنت المربوط بالـ QR (زي واتساب ويب). وده برضه السبب إن الأزرار ممكن متظهرش على كل الموبايلات — والبديل المرقّم بيغطي ده.

العملاء هيشوفوا الأزرار دايماً؟

مش دايماً. بعض الموبايلات بتعرض الرسايل التفاعلية كنص عادي. ومع البديل المرقّم (الافتراضي) العميل يقدر يرد بكتابة الرقم أو اسم الاختيار، ويوصلك نفس interactive.reply في الحالتين.

لو العميل كتب حاجة تانية؟

بتوصلك كرسالة عادية message.received. والتفاعل بيفضل مفتوح لحد ما يترد عليه أو مدته تخلص.

إزاي أمنع إن الحدث يتنفّذ مرتين؟

احفظ id الحدث وتجاهل اللي شفته قبل كده — إعادة المحاولة وإعادة الإرسال اليدوي بيستخدموا نفس الـ id. وفي الإرسال ابعت دايماً Idempotency-Key.

الويب هوك بتاعي اتوقف. ليه؟

رابطك رد بـ 410 Gone. صلّح الـ endpoint وبعدين احفظ الرابط تاني من تبويب المطوّرين — بيشتغل على طول.

مرسلات بتضمن إن رقمي مش هيتحظر؟

مفيش أداة تقدر تضمن ده. درع الرقم بيقلّل الخطر بالفواصل والتسخين وقواعد المحتوى — وراسل ناس مستنية تسمع منك.

أقدر أبعت أنهي أنواع رسايل؟

نص، صور، فيديو (وكمان كـ GIF أو فيديو دائري)، ملفات صوت، رسايل صوتية، مستندات، ملصقات، مواقع، جهات اتصال، استطلاعات، مناسبات (تجريبي)، أزرار رد، قوائم، وزرار رابط — كلهم من POST /whatsapp/public/messages. الموقع المباشر والمدفوعات ورسايل الكتالوج والطلبات والحالات والمكالمات والقوالب مش ممكنة من رقم مربوط.

كل الموبايلات هتعرض كل الأنواع؟

النص والميديا والملصقات والمواقع وجهات الاتصال والاستطلاعات بتظهر على كل إصدارات واتساب الحديثة. الأزرار والقوائم ممكن تظهر كنص عادي على بعض الموبايلات (والبديل المرقّم بيغطي ده)؛ والعرض مرة واحدة والفيديو الدائري محتاجين تطبيق حديث؛ والمناسبات غالباً بتظهر في الجروبات بس.

روابط الميديا صالحة لحد إمتى؟

24 ساعة. بعدها حمّل بمفتاح الـ API (صلاحية media).

أجرّب إزاي من غير ما أزعج العملاء؟

استخدم إرسال حدث تجريبي للويب هوك، وابعت رسايل الـ API لرقم موبايلك أنت.

↑ لأعلى الصفحة