إرسال الرسائل
نقطة واحدة ترسل كل رسالة. تقبل الرسالة وتردّ فورا وتسلّم لاحقا — فالمهم إذن هو ما يقبله الطلب، وماذا تعني الحالات، وكيف تجعل إعادة المحاولة آمنة.
إرسال رسالة
POST/v1/messagesرد 201 يعني القبول لا التسليم. تُوضع الرسالة في طابور ويوزّعها عامل مستقل؛ تابعها عبر نقاط الرسائل أو عبر الـ webhooks.
curl -X POST https://whats.azzamkh.sa/api/v1/messages \
-H "Authorization: Bearer $WA_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"channel_id": "ch_...",
"to": "+9665XXXXXXXX",
"type": "text",
"text": { "body": "Your code is 481902" }
}'
# 201 Created
# {
# "id": "msg_...",
# "status": "queued",
# "channel_id": "ch_...",
# "to": "+9665XXXXXXXX",
# "type": "text",
# "category": "utility",
# "created_at": "2026-08-13T10:00:00.000Z"
# }جسم الطلب
المطلوب دائما هو to وحده. وtext مطلوب حين يكون النوع نصا، وtemplate.slug حين يكون قالبا.
| الحقل | النوع | مطلوب | الوصف |
|---|---|---|---|
| to | string | مطلوب | المستلم بصيغة E.164 الصارمة، مثل +9665XXXXXXXX. وأي شكل آخر خطأ تحقق يسمّي الحقل. |
| type | "text" | "template" | اختياري | الافتراضي text. |
| text.body | string | اختياري | نص الرسالة، من 1 إلى 4096 حرفا. مطلوب للرسالة النصية. |
| template.slug | string | اختياري | معرّف القالب النصي. مطلوب لرسالة القالب. |
| template.language | string | اختياري | لغة القالب. القالب يُعرَّف بالـ slug واللغة معا. |
| template.variables | object<string, string> | اختياري | قيم متغيرات القالب. والقيمة المطلوبة الناقصة تعطي 422. |
| channel_id | string | اختياري | أغفله فتُستخدم القناة الافتراضية لمساحة العمل. وسمّه حين يكون لديك أكثر من قناة. |
| category | "authentication" | "utility" | "marketing" | اختياري | تحدد أولوية الطابور. الافتراضي utility. وفئة القالب نفسه تعلو على ما ترسله هنا. |
الحقول المجهولة تُرفض
جسم الطلب وكلا كائنيه الفرعيين صارمة: الحقل المكتوب خطأ يعطي 422 برمز VALIDATION_ERROR يسمّيه، لا قيمة تُتجاهل بصمت. وهذا هو السلوك المرغوب — فخطأ إملائي في اسم حقل يخفق بصوت عال بدل أن يرسل رسالة ناقصة.
قائمة الأنواع هي بالضبط "text", "template". وإرسال صورة أو مستند أو صوت أو فيديو غير متاح بعد على هذه النقطة؛ أما الوسائط الواردة فتصل وتُسجّل على الرسالة.
الاستجابة
استجابة الإرسال صغيرة عمدا. اقرأ الرسالة إن احتجت conversation_id أو معرّف المزوّد أو خط الزمن — فلا شيء من ذلك موجود لحظة القبول.
| الحقل | النوع | الوصف |
|---|---|---|
| id | string | المعرّف العام للرسالة. استخدمه في كل متابعة. |
| status | string | دائما queued عند القبول. |
| channel_id | string | القناة التي سترسل منها الرسالة، بعد استنتاجها إن أغفلتها. |
| to | string | المستلم بعد التطبيع إلى E.164. |
| type | string | يعيد النوع المطلوب. |
| category | string | الفئة بعد التصنيف، وقد تختلف عمّا أرسلته إن تجاوزها قالب. |
| created_at | string | وقت إنشاء صف الرسالة. |
إرسال قالب
اجعل النوع template وسمّ الـ slug واللغة. راجع القوالب لمعرفة كيف يُعرَّف المحتوى ومتغيراته.
curl -X POST https://whats.azzamkh.sa/api/v1/messages \
-H "Authorization: Bearer $WA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"channel_id": "ch_...",
"to": "+9665XXXXXXXX",
"type": "template",
"category": "utility",
"template": {
"slug": "order_confirmation",
"language": "ar",
"variables": { "order_id": "A-10428", "name": "سارة" }
}
}'الفئات والأولوية
تصف الفئة الغرض من الرسالة، وتستخدمها المنصة لتقرر ما يتقدم حين تمتلئ القناة.
| الفئة | الأولوية | الوصف |
|---|---|---|
| authentication | الأعلى | كلمات المرور لمرة واحدة ورموز التحقق. حساسة للوقت: الرمز المتأخر بلا قيمة. |
| utility | عادية | رسائل معاملات عن شيء فعله المستلم أصلا — طلب أو حجز أو شحنة. |
| marketing | الأدنى | محتوى ترويجي. الأولوية الأدنى، والفئة الأكثر تقييدا بالسياسات. |
إغفال الفئة يصنّف الرسالة على أنها utility.
الأولوية ترتّب الطابور ولا تزيد الطاقة
حين تبلغ القناة معدل تمريرها تتقدم رسالة المصادقة على الترويجية. وحين لا تبلغه لا تغيّر الأولوية شيئا. وهي ليست وسيلة لرفع حد.
الحماية من التكرار
أرسل header باسم Idempotency-Key مع كل إرسال. هو اختياري، وهو الفرق بين انقطاع شبكة لا يكلّفك شيئا وانقطاع يكلّفك رسالة مكررة.
| الحالة | النتيجة |
|---|---|
| أول استعمال للمفتاح | تُنشأ الرسالة وتُخزَّن الاستجابة مقابل المفتاح. |
| المفتاح نفسه وجسم مطابق | تُعاد الاستجابة المخزنة حرفا بحرف مع header باسم Idempotent-Replay قيمته true. ولا تُنشأ رسالة ثانية. |
| المفتاح نفسه وجسم مختلف | 409 برمز IDEMPOTENCY_CONFLICT. ولا يُرسل شيء. |
| المفتاح نفسه والطلب الأول ما زال يعمل | 409 برمز CONFLICT و details.reason يساوي idempotent_request_in_progress. أعد المحاولة بعد قليل. |
| الطلب الأول أخفق | يُحرَّر المفتاح، فيمكن إعادة المحاولة به نفسه. وترى الخطأ الأصلي. |
KEY=$(uuidgen)
# First call: the message is created.
curl -X POST https://whats.azzamkh.sa/api/v1/messages \
-H "Authorization: Bearer $WA_API_KEY" \
-H "Idempotency-Key: $KEY" \
-d '{ "to": "+9665XXXXXXXX", "type": "text", "text": { "body": "Hello" } }'
# 201 Created { "id": "msg_A", "status": "queued", ... }
# Same key, IDENTICAL body: the stored response is replayed byte for byte.
# No second message is created.
# 201 Created
# idempotent-replay: true
# { "id": "msg_A", "status": "queued", ... }
# Same key, DIFFERENT body:
# 409 Conflict
# { "error": { "code": "IDEMPOTENCY_CONFLICT", ... } }- تُقارن الأجسام بتجزئة صورتها القياسية، فترتيب المفاتيح والمسافات لا يهم — القيم وحدها هي التي تهم.
- المفاتيح محصورة بمساحة عملك. وعميلان يمكن أن يستعملا القيمة نفسها دون تصادم.
- يُحفظ المفتاح
24ساعة ثم يُنسى. وإعادة استعماله بعدها ترسل رسالة جديدة. - طول المفتاح
255حرفا بحد أقصى. والـ UUID اختيار جيد.
الحالات وخط الزمن
docs.pages.messages.lifecycle.body
| الحالة | الوصف | الانتقالات المسموحة |
|---|---|---|
| created | الصف موجود ولم يُوضع في طابور بعد. | queued, cancelled, failed_permanent |
| queued | مقبولة وتنتظر عاملا. وهذا ما يعيده الإرسال. | dispatching, cancelled, expired, failed_retryable, failed_permanent |
| dispatching | التقطها عامل وهو يخاطب المزوّد. | provider_accepted, sent, failed_retryable, failed_permanent, cancelled |
| provider_accepted | أخذها المزوّد ولم يؤكد الإرسال بعد. | sent, delivered, read, failed_retryable, failed_permanent, expired |
| sent | سُلّمت إلى واتساب. | delivered, read, failed_permanent, expired |
| delivered | وصلت إلى جهاز المستلم. | read, failed_permanent |
| read | قرأها المستلم، حيث يبلّغ المزوّد بذلك. | — |
| failed_retryable | أخفقت إخفاقا ستُعاد المحاولة بعده. وليست نهائية. | queued, dispatching, failed_permanent, cancelled, expired |
| failed_permanent | أخفقت نهائيا. نهائية. | — |
| cancelled | أُوقفت قبل الإرسال — مثلا حين تعذّر بلوغ طابور التوزيع. | — |
| expired | انقضت نافذة تسليمها قبل أن تُرسل. | — |
أربع حالات نهائية لا يتبعها شيء: read, failed_permanent, cancelled, expired.
لا توجد حالة failed مجردة
الإخفاق إما failed_retryable وإما failed_permanent، والاثنان يعنيان لكودك أمرين متعاكسين. ومعاملتهما واحدا أشيع خطأ هنا: فالإخفاق القابل لإعادة المحاولة ما زال في الطريق.
قراءة الرسائل
المرشّحات
| المعامل | القيم | الوصف |
|---|---|---|
| status | created, queued, dispatching, provider_accepted, … | الترشيح بالحالة. ويُقبل state اسما بديلا للمعامل نفسه. |
| direction | outbound, inbound | الرسائل الصادرة التي أرسلتها أو الواردة التي استقبلتها. |
| category | authentication, utility, marketing | الترشيح بالفئة. |
| channel_id | ch_… | الحصر بقناة واحدة. ومعرّف قناة مجهول يعطي 404 لا صفحة فارغة. |
| limit, after | — | تقسيم بالـ cursor. الأحدث أولا. |
قيمة المرشّح المجهولة خطأ
على خلاف بعض القوائم الأخرى في هذه الـ API، ترفض مرشّحات الرسائل القيمة التي لا تعرفها بـ 422 يسمّي المجموعة المسموحة. فخطأ إملائي في مرشّح لا يعيد لك كل شيء بصمت أبدا.
رسالة واحدة مع خط زمنها
قراءة رسالة واحدة تضيف مصفوفة events، الأقدم أولا: كل انتقال حالة مع مصدره ورقم محاولته وسببه. وهذا هو السجل الذي تنظر فيه حين لا تصل رسالة.
curl https://whats.azzamkh.sa/api/v1/messages/msg_... \
-H "Authorization: Bearer $WA_API_KEY"
# 200 OK
# {
# "id": "msg_...",
# "status": "delivered",
# "direction": "outbound",
# "attempts": 1,
# "queued_at": "2026-08-13T10:00:00.000Z",
# "sent_at": "2026-08-13T10:00:01.140Z",
# "delivered_at": "2026-08-13T10:00:02.980Z",
# "read_at": null,
# "events": [
# { "id": "mev_...", "from_status": null, "status": "created", ... },
# { "id": "mev_...", "from_status": "created", "status": "queued", ... },
# { "id": "mev_...", "from_status": "queued", "status": "dispatching", ... },
# { "id": "mev_...", "from_status": "dispatching", "status": "sent", ... },
# { "id": "mev_...", "from_status": "sent", "status": "delivered", ... }
# ]
# }ما قد يخفق
كل هذه تتبع الغلاف الموصوف في الأخطاء والحدود. أما الإخفاق بعد القبول فليس خطأ HTTP أصلا — بل يصل تغيّرَ حالة وwebhook.
| الرمز | HTTP | متى يحدث |
|---|---|---|
| VALIDATION_ERROR | 422 | جسم غير سليم: مستلم بغير صيغة E.164، أو نص ناقص، أو حقل مجهول. |
| CHANNEL_NOT_FOUND | 404 | القناة المسمّاة غير موجودة في مساحة العمل هذه. |
| CHANNEL_NOT_CONNECTED | 409 | القناة موجودة لكنها غير متصلة. |
| CHANNEL_CAPABILITY_UNAVAILABLE | 409 | القناة لا تستطيع ما تحتاجه الرسالة، أو لم تُحدَّث قدراتها قط. |
| SANDBOX_TEST_KEY_REQUIRED | 403 | استُعمل مفتاح live على قناة Sandbox. |
| SANDBOX_RECIPIENT_NOT_VERIFIED | 403 | لم يوثّق المستلم ملكيته على هذه الجلسة، أو هو رقم غير الرقم الموثّق. |
| TEMPLATE_NOT_SENDABLE | 409 | القالب مسودة أو موقوف أو معطّل. |
| TEMPLATE_VARIABLE_MISSING | 422 | متغيّر قالب مطلوب بلا قيمة. وتسرد details أيّها. |
| ENTITLEMENT_LIMIT_REACHED | 409 | بلغت حدّ خطة. وتسمّي details الاستحقاق والحد والعدد الحالي. |
| IDEMPOTENCY_CONFLICT | 409 | استُعمل مفتاح الحماية من التكرار من قبل بجسم مختلف. |
| QR_SAFETY_LIMIT_REACHED | 429 | استُنفد الحد الأمني لقناة الـ QR في النافذة الحالية. |
| SERVICE_UNAVAILABLE | 503 | طابور التوزيع متعذّر. تُلغى الرسالة بدل تركها معلّقة؛ أعد الإرسال. |