ابدأ في 5 دقائق #
المطلوب خمس خطوات: مفتاح، ثم معرّف القناة، ثم أول رسالة، ثم متابعة حالتها، ثم استقبال ردود العملاء.
- أنشئ مفتاح API. من لوحة التحكم افتح «واجهة API للمطورين» ثم «المفاتيح» ثم أنشئ مفتاحًا. يظهر الرمز مرة واحدة فقط، فانسخه واحفظه على الخادم. تحتاج رصيد رسائل API في مساحة العمل، وإلا يرد الطلب بالكود
api_not_enabled. - اعرف معرّف القناة التي سترسل منها:
GET /v1/numbersثم انسخ قيمة id لقناة حالتها connected. - أرسل أول رسالة:
POST /v1/messages/send. يرد الخادم فورًا بالرمز 202 ومعه معرّف الرسالة، والإرسال الفعلي يتم بعدها. - تابع الحالة:
GET /v1/messages/show/{id}. تمر الرسالة بالحالات queued ثم sent ثم delivered ثم read، أو failed. - استقبل الردود: من «الويب هوك» أضف رابط HTTPS على خادمك واختر الحدث
message.received. يصلك POST موقّع عند كل رسالة جديدة من عميل.
الخطوة 2: القنوات
curl https://derha.net/api/v1/numbers \
-H "Authorization: Bearer mh_live_xxxxxxxxxxxxxxxx_your_secret"الخطوة 3: أول رسالة
curl -X POST https://derha.net/api/v1/messages/send \
-H "Authorization: Bearer mh_live_xxxxxxxxxxxxxxxx_your_secret" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-4821" \
-d '{
"number_id": "7f2a1c9e-3b4d-5e6f-7a8b-9c0d1e2f3a4b",
"to": "201014642581",
"type": "text",
"text": "Hello, your order #4821 has shipped."
}'الخطوة 4: الحالة
curl https://derha.net/api/v1/messages/show/3c9d8e7f-1a2b-3c4d-5e6f-7a8b9c0d1e2f \
-H "Authorization: Bearer mh_live_xxxxxxxxxxxxxxxx_your_secret"للذكاء الاصطناعي والأدوات #
هذه الصفحة مكتوبة بـ HTML ثابت بلا جافاسكربت، ويقرؤها أي زاحف أو نموذج ذكاء اصطناعي مباشرة. لمن يفضّل صيغة أخف أو صيغة آلية توجد هذه الملفات العامة:
| الملف | ما هو |
|---|---|
https://derha.net/llms.txt | فهرس قصير للتوثيق حسب معيار llms.txt، مع روابط كل الملفات. |
https://derha.net/llms-full.txt | التوثيق الكامل في ملف Markdown واحد بالإنجليزية. ألصقه كما هو في سياق النموذج. |
https://derha.net/llms-full.ar.txt | النسخة الكاملة نفسها بالعربية. |
https://derha.net/api-docs/openapi.json | مواصفة OpenAPI 3.1 لكل المسارات. تصلح لتوليد عميل برمجي أو لتعريف أدوات لنموذج ذكاء اصطناعي. |
ملخص لنموذج يكتب الكود
- العنوان الأساس: https://
derha.net/api/v1. الصيغة JSON بترميز UTF-8. HTTPS فقط. - المصادقة: ترويسة Authorization: Bearer <token> في كل طلب. لا تضع الرمز في كود المتصفح.
- كل استجابة بالشكل { success, message, data } عند النجاح، و { success:false, code, message } عند الخطأ. ابنِ المنطق على code وليس على message.
- الإرسال غير متزامن: POST /messages/send يرد 202 فقط، والنتيجة النهائية تأتي من GET /messages/show/{id} أو من ويب هوك.
- أرسل
Idempotency-Keyمع كل إرسال حتى لا تتكرر الرسالة عند إعادة المحاولة. - حدّد المستلم بطريقة واحدة فقط: to مع
number_id، أوconversation_idوحده، أوcontact_idمعnumber_id. - تحقق من توقيع كل ويب هوك قبل الوثوق به، واستعمل معرّف الحدث لتجاهل التكرار.
- لا تخترع مسارات أو حقولًا غير موجودة هنا. المسارات المذكورة في هذا التوثيق هي كل ما يتاح.
المفاهيم الأساسية #
| المفهوم | المعنى |
|---|---|
| مساحة العمل | الحساب الذي يملك المفاتيح والرصيد. كل مفتاح يعمل داخل مساحة عمل واحدة فقط ولا يرى غيرها. |
| الشركة | نشاط داخل مساحة العمل، وله قنواته وعملاؤه. المعرّف: business_id. |
| القناة | خط التواصل الذي تخرج منه الرسائل: رقم واتساب، أو حساب ماسنجر أو إنستغرام أو تيليغرام أو تيك توك. معرّفها number_id (واسمه الآخر channel_id). |
| جهة الاتصال | العميل. قد يكون له رقم هاتف، وقد يُعرف بمعرّف المنصة فقط. المعرّف: contact_id. |
| المحادثة | الخيط بين قناة وجهة اتصال. المعرّف: conversation_id. للرد على محادثة تعرفها أرسل conversation_id وحده. |
| الرسالة | نص أو ملف، واردة (inbound) أو صادرة (outbound). |
| الويب هوك | رابط HTTPS على خادمك يستقبل أحداثًا موقّعة. لا يحتاج مفتاح API. |
المصادقة #
كل طلب يحمل رمز المفتاح في ترويسة Authorization بنمط Bearer. يُصدَر الرمز مرة واحدة عند إنشاء المفتاح ولا يمكن استرجاعه بعد ذلك. الرمز يبدأ بـ mh_live.
Authorization: Bearer mh_live_xxxxxxxxxxxxxxxx_your_secretيمكن تقييد المفتاح بعناوين IP أو بأصول (origin) محددة. الطلب من غير المسموح يرد بـ ip_not_allowed أو origin_not_allowed. الطلب عبر HTTP العادي يُرفض بـ insecure_transport.
القواعد العامة #
غلاف الاستجابة
كل الاستجابات بالشكل نفسه. النجاح يحمل success=true مع كائن data. الخطأ يحمل success=false مع code ثابت (آمن للبناء عليه) ورسالة مقروءة.
// success
{ "success": true, "message": "...", "data": { } }
// error
{ "success": false, "code": "validation_failed", "message": "..." }منع التكرار (Idempotency)
عند الإرسال أضف ترويسة Idempotency-Key بقيمة فريدة لكل عملية، مثل رقم الطلب. إذا أعدت الطلب بالقيمة نفسها بسبب انقطاع الشبكة تحصل على النتيجة نفسها دون إرسال الرسالة مرتين. استعمال القيمة نفسها مع جسم مختلف يرد بـ idempotency_key_reused، والطلب الذي ما زال قيد المعالجة يرد بـ request_in_progress.
حدود المعدل
الطلبات محدودة لكل مفتاح ولكل عنوان IP. عند تجاوز الحد يرد 429 مع الكود rate_limited وترويسة Retry-After فيها عدد الثواني قبل إعادة المحاولة. انتظر هذه المدة ثم أعد الطلب.
الترقيم
نقاط القوائم تقبل page وper_page (الأقصى 100) وتعيد meta.pagination بالحقول total وper_page وcurrent_page وlast_page.
الأرقام والتواريخ
- أرقام الهاتف بأرقام دولية بلا علامة +، مثل 201014642581.
- التواريخ بصيغة ISO 8601 مثل 2026-10-03T08:30:00+00:00.
- المعرّفات UUID. نص الرسالة يُحفظ ويُعاد كما أُرسل تمامًا.
المسارات #
| الطريقة | المسار | الاستخدام |
|---|---|---|
GET | /v1/businesses | عرض الشركات والقنوات |
GET | /v1/businesses/{id} | تفاصيل شركة |
GET | /v1/numbers | القنوات |
POST | /v1/messages/send | إرسال رسالة |
GET | /v1/custom-messages | الرسائل المخصصة |
POST | /v1/messages/send-custom | إرسال رسالة مخصصة |
GET | /v1/messages/show/{id} | حالة رسالة |
GET | /v1/messages/list | قائمة الرسائل المُرسَلة |
GET | /v1/messages/inbound | الرسائل الواردة |
GET | /v1/media/{id} | تنزيل ملف |
GET | /v1/balance | الرصيد |
GET | /v1/webhooks/events | أحداث الويب هوك |
GET | /v1/webhooks | عرض الويب هوك |
POST | /v1/webhooks/{id}/test | إرسال حدث تجريبي |
GET | /v1/webhooks/deliveries | سجل التسليم |
POST | /v1/webhooks/deliveries/{id}/redeliver | إعادة إرسال حدث |
عرض الشركات والقنوات
GET/v1/businesses
يعيد كل الشركات في مساحة عملك مع قنواتها. استعمل number_id لكل قناة لاختيار القناة المُرسِلة.
curl https://derha.net/api/v1/businesses \
-H "Authorization: Bearer mh_live_xxxxxxxxxxxxxxxx_your_secret"الاستجابة 200 OK
{
"success": true,
"message": "...",
"data": {
"businesses": [
{
"id": "9b1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e",
"name": "Al Amal Store",
"numbers": [
{
"number_id": "7f2a1c9e-3b4d-5e6f-7a8b-9c0d1e2f3a4b",
"name": "Sales",
"phone": "9665xxxxxxxx",
"channel": "whatsapp",
"status": "connected",
"connected": true
}
]
}
]
}
}تفاصيل شركة
GET/v1/businesses/{id}
يعيد شركة واحدة بمعرّفها مع قنواتها. شركة خارج مساحة عملك ترد بـ 404 والكود business_not_found.
curl https://derha.net/api/v1/businesses/9b1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e \
-H "Authorization: Bearer mh_live_xxxxxxxxxxxxxxxx_your_secret"القنوات
GET/v1/numbers
قنوات مساحة العمل: المعرّف id (وهو number_id)، والمنصة channel، والاسم العام handle حيث لا يوجد رقم هاتف، وهل تستطيع القناة بدء محادثة can_initiate (any أو known أو none)، وهل هي متصلة الآن connected. المسار /v1/channels يعيد القائمة نفسها تحت المفتاح channels.
curl https://derha.net/api/v1/numbers \
-H "Authorization: Bearer mh_live_xxxxxxxxxxxxxxxx_your_secret"الاستجابة 200 OK
{
"success": true,
"data": {
"numbers": [
{
"id": "7f2a1c9e-3b4d-5e6f-7a8b-9c0d1e2f3a4b",
"business_id": "9b1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e",
"name": "Sales",
"phone": "201014642581",
"channel": "whatsapp",
"handle": null,
"can_initiate": "any",
"status": "connected",
"connected": true,
"official": true,
"mode": "conversations",
"created_at": "2026-09-01T10:00:00+00:00"
}
]
}
}إرسال رسالة
POST/v1/messages/send
يضع رسالة نصية في طابور الإرسال ويرد 202 فورًا. تابع حالتها بعد ذلك بنقطة «حالة رسالة» أو بالويب هوك.
الترويسات
| الاسم | النوع | إلزامي | الوصف |
|---|---|---|---|
Idempotency-Key | string | لا | قيمة فريدة تمنع تكرار الإرسال عند إعادة المحاولة. |
معطيات الجسم (JSON)
حدّد المستلم بطريقة واحدة فقط: to مع number_id، أو conversation_id وحده، أو contact_id مع number_id. إرسال أكثر من طريقة يرد بـ validation_failed.
| الاسم | النوع | إلزامي | الوصف |
|---|---|---|---|
number_id | uuid | لا | معرّف القناة المُرسِلة. مطلوب مع to أو contact_id. |
channel_id | uuid | لا | اسم آخر لـ number_id، للقنوات التي ليست أرقامًا. |
conversation_id | uuid | لا | محادثة موجودة على أي منصة. تخرج الرسالة على قناتها، فلا يلزم number_id. |
contact_id | uuid | لا | جهة اتصال لها حساب على منصة القناة المختارة. تُفتح محادثتها إذا لم تكن موجودة. |
to | string | لا | رقم المستلم بالصيغة الدولية مع رمز الدولة، مثل 201014642581. يُقبل + والمسافات ويُزالان. الرقم المحلي بلا رمز دولة (01014642581) يُرفض. |
text | string | نعم | نص الرسالة، حتى 4096 حرفًا. |
type | string | لا | نوع الرسالة. المدعوم حاليًا text وهو الافتراضي. |
transactional | boolean | لا | اجعله true عندما تخص الرسالة طلب العميل أو حجزه أو حسابه (إيصال، موعد، رمز). يلزم فقط لعميل طلب إيقاف الرسائل. انظر «العملاء الذين طلبوا التوقف». |
curl -X POST https://derha.net/api/v1/messages/send \
-H "Authorization: Bearer mh_live_xxxxxxxxxxxxxxxx_your_secret" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-4821" \
-d '{
"number_id": "7f2a1c9e-3b4d-5e6f-7a8b-9c0d1e2f3a4b",
"to": "201014642581",
"type": "text",
"text": "Hello, your order #4821 has shipped."
}'curl -X POST https://derha.net/api/v1/messages/send \
-H "Authorization: Bearer mh_live_xxxxxxxxxxxxxxxx_your_secret" \
-H "Content-Type: application/json" \
-d '{
"conversation_id": "9c0d1e2f-3a4b-5c6d-7e8f-9a0b1c2d3e4f",
"text": "We received your message."
}'const res = await fetch("https://derha.net/api/v1/messages/send", {
method: "POST",
headers: {
"Authorization": "Bearer " + process.env.MESSAGEHUB_TOKEN,
"Content-Type": "application/json",
"Idempotency-Key": "order-4821",
},
body: JSON.stringify({
number_id: "7f2a1c9e-3b4d-5e6f-7a8b-9c0d1e2f3a4b",
to: "201014642581",
type: "text",
text: "Hello, your order #4821 has shipped.",
}),
})
const data = await res.json()
if (!data.success) throw new Error(data.code)import os, requests
res = requests.post(
"https://derha.net/api/v1/messages/send",
headers={
"Authorization": "Bearer " + os.environ["MESSAGEHUB_TOKEN"],
"Idempotency-Key": "order-4821",
},
json={
"number_id": "7f2a1c9e-3b4d-5e6f-7a8b-9c0d1e2f3a4b",
"to": "201014642581",
"type": "text",
"text": "Hello, your order #4821 has shipped.",
},
timeout=10,
)
data = res.json()
if not data["success"]:
raise RuntimeError(data["code"])$response = Http::withToken(env('MESSAGEHUB_TOKEN'))
->withHeaders(['Idempotency-Key' => 'order-4821'])
->post('https://derha.net/api/v1/messages/send', [
'number_id' => '7f2a1c9e-3b4d-5e6f-7a8b-9c0d1e2f3a4b',
'to' => '201014642581',
'type' => 'text',
'text' => 'Hello, your order #4821 has shipped.',
]);الاستجابة 202 Accepted
{
"success": true,
"message": "...",
"data": {
"message": {
"id": "3c9d8e7f-1a2b-3c4d-5e6f-7a8b9c0d1e2f",
"number_id": "7f2a1c9e-3b4d-5e6f-7a8b-9c0d1e2f3a4b",
"to": "201014642581",
"type": "text",
"status": "queued",
"created_at": "2026-07-11T10:00:00+00:00"
}
}
}الرسائل المخصصة
GET/v1/custom-messages
الرسائل الجاهزة في مساحة العمل وما تتوقعه كل خانة فيها: هل تُملأ من بيانات العميل أم ترسلها أنت، وهل تُحفظ بعد الإرسال. المعرّف الذي يبدأ بـ lib: رسالة جاهزة من مكتبة المنصة وتُرسل بالطريقة نفسها. إنشاء الرسائل يتم من لوحة التحكم فقط.
curl https://derha.net/api/v1/custom-messages \
-H "Authorization: Bearer mh_live_xxxxxxxxxxxxxxxx_your_secret"إرسال رسالة مخصصة
POST/v1/messages/send-custom
يرسل رسالة جاهزة بعد ملء خاناتها. الخانات المربوطة ببيانات العميل تُحل على الخادم وتتجاهل أي قيمة ترسلها لها. المستلم يُحدد بنفس الطرق الثلاث لنقطة الإرسال.
الترويسات
| الاسم | النوع | إلزامي | الوصف |
|---|---|---|---|
Idempotency-Key | string | لا | أرسله دائمًا مع رموز التحقق: إعادة المحاولة بالمفتاح نفسه لا ترسل رمزًا ثانيًا. |
معطيات الجسم (JSON)
| الاسم | النوع | إلزامي | الوصف |
|---|---|---|---|
number_id | uuid | لا | معرّف القناة المُرسِلة. مطلوب مع to أو contact_id. |
channel_id | uuid | لا | اسم آخر لـ number_id، للقنوات التي ليست أرقامًا. |
conversation_id | uuid | لا | محادثة موجودة على أي منصة. تخرج الرسالة على قناتها، فلا يلزم number_id. |
contact_id | uuid | لا | جهة اتصال لها حساب على منصة القناة المختارة. تُفتح محادثتها إذا لم تكن موجودة. |
to | string | لا | رقم المستلم بالصيغة الدولية مع رمز الدولة، مثل 201014642581. يُقبل + والمسافات ويُزالان. الرقم المحلي بلا رمز دولة (01014642581) يُرفض. |
custom_message_id | string | نعم | معرّف الرسالة المخصصة، أو lib:key لرسالة جاهزة. |
values | object | لا | قيم الخانات، ومفتاح كل قيمة هو رقم الخانة (المفتاح "2" وقيمته "482913"). ليست قائمة، لأن الخانات المربوطة ببيانات العميل لا تظهر هنا. |
curl -X POST https://derha.net/api/v1/messages/send-custom \
-H "Authorization: Bearer mh_live_xxxxxxxxxxxxxxxx_your_secret" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: otp-2026-09-03-1" \
-d '{
"number_id": "7f2a1c9e-3b4d-5e6f-7a8b-9c0d1e2f3a4b",
"to": "201014642581",
"custom_message_id": "lib:otp",
"values": { "2": "482913", "3": "10 minutes" }
}'حالة رسالة
GET/v1/messages/show/{id}
تفاصيل رسالة أرسلتها بالـ API: الحالة وكل الأوقات وسبب الفشل والخط الزمني الكامل history. لا تظهر هنا الرسائل التي لم تنشأ من الـ API (الرسائل الواردة لها نقطتها الخاصة).
curl https://derha.net/api/v1/messages/show/3c9d8e7f-1a2b-3c4d-5e6f-7a8b9c0d1e2f \
-H "Authorization: Bearer mh_live_xxxxxxxxxxxxxxxx_your_secret"الاستجابة 200 OK
{
"success": true,
"message": "...",
"data": {
"message": {
"id": "3c9d8e7f-1a2b-3c4d-5e6f-7a8b9c0d1e2f",
"send_type": "single",
"number_id": "7f2a1c9e-3b4d-5e6f-7a8b-9c0d1e2f3a4b",
"to": "201014642581",
"type": "text",
"text": "Hello, your order #4821 has shipped.",
"status": "delivered",
"failure_reason": null,
"failure_category": null,
"failure_message": null,
"created_at": "2026-07-11T10:00:00+00:00",
"sent_at": "2026-07-11T10:00:01+00:00",
"delivered_at": "2026-07-11T10:00:04+00:00",
"read_at": null,
"failed_at": null,
"history": [
{ "at": "2026-07-11T10:00:00+00:00", "event": "queued" },
{ "at": "2026-07-11T10:00:01+00:00", "event": "attempt", "attempt": 1 },
{ "at": "2026-07-11T10:00:01+00:00", "event": "sent" },
{ "at": "2026-07-11T10:00:04+00:00", "event": "delivered" }
]
}
}
}قائمة الرسائل المُرسَلة
GET/v1/messages/list
ملخص مرقّم لعمليات الإرسال بالـ API، الأحدث أولًا. لتفاصيل رسالة استعمل /v1/messages/show/{id}.
معطيات الاستعلام
| الاسم | النوع | إلزامي | الوصف |
|---|---|---|---|
business_id | uuid | لا | رسائل شركة محددة فقط. |
number_id | uuid | لا | رسائل قناة محددة فقط. |
status | string | لا | queued أو sent أو delivered أو read أو failed. |
page | integer | لا | رقم الصفحة، يبدأ من 1. |
per_page | integer | لا | عدد العناصر في الصفحة. الافتراضي 25 والأقصى 100. |
curl "https://derha.net/api/v1/messages/list?number_id=7f2a1c9e-3b4d-5e6f-7a8b-9c0d1e2f3a4b&status=delivered&per_page=25" \
-H "Authorization: Bearer mh_live_xxxxxxxxxxxxxxxx_your_secret"الرسائل الواردة
GET/v1/messages/inbound
الرسائل التي أرسلها العملاء إلى قنواتك، الأحدث أولًا، بنفس شكل message في الويب هوك. استعملها لتعويض أحداث فاتتك أثناء توقف خادمك، أو إذا فضّلت الاستعلام الدوري على الويب هوك.
معطيات الاستعلام
| الاسم | النوع | إلزامي | الوصف |
|---|---|---|---|
business_id | uuid | لا | شركة محددة. |
number_id | uuid | لا | قناة محددة (أو channel_id). |
channel | string | لا | منصة محددة: whatsapp أو messenger أو instagram أو telegram أو tiktok. |
since | ISO 8601 | لا | الرسائل من هذا الوقت فصاعدًا. |
until | ISO 8601 | لا | الرسائل حتى هذا الوقت. |
page | integer | لا | رقم الصفحة، يبدأ من 1. |
per_page | integer | لا | عدد العناصر في الصفحة. الافتراضي 25 والأقصى 100. |
curl "https://derha.net/api/v1/messages/inbound?since=2026-10-03T00:00:00Z&per_page=50" \
-H "Authorization: Bearer mh_live_xxxxxxxxxxxxxxxx_your_secret"تنزيل ملف
GET/v1/media/{id}
يعيد محتوى ملف مرفق برسالة (صورة أو صوت أو فيديو أو مستند) عند الرابط url الموجود في المرفق. يحتاج مفتاح API كأي طلب آخر.
curl -o photo.jpg "https://derha.net/api/v1/media/01a0ff03-16e0-7350-a174-d09c254ee041" \
-H "Authorization: Bearer mh_live_xxxxxxxxxxxxxxxx_your_secret"الرصيد
GET/v1/balance
رصيد الرسائل الذي يُخصم منه كل إرسال، ونقاط الذكاء الاصطناعي. افحصه قبل دفعة كبيرة.
curl https://derha.net/api/v1/balance \
-H "Authorization: Bearer mh_live_xxxxxxxxxxxxxxxx_your_secret"الاستجابة 200 OK
{
"success": true,
"data": {
"api_enabled": true,
"messages": { "granted": 10000, "used": 2310, "remaining": 7690, "unlimited": false },
"ai_points": { "granted": 500, "used": 120, "remaining": 380, "unlimited": false }
}
}أحداث الويب هوك
GET/v1/webhooks/events
قائمة الأحداث المتاحة ومعنى كل واحد.
curl https://derha.net/api/v1/webhooks/events \
-H "Authorization: Bearer mh_live_xxxxxxxxxxxxxxxx_your_secret"عرض الويب هوك
GET/v1/webhooks
كل الويب هوك في مساحة العمل: الرابط والأحداث والقنوات وهل هو موقوف. لا تظهر الأسرار. الإنشاء والتعديل من لوحة التحكم فقط.
curl https://derha.net/api/v1/webhooks \
-H "Authorization: Bearer mh_live_xxxxxxxxxxxxxxxx_your_secret"إرسال حدث تجريبي
POST/v1/webhooks/{id}/test
يرسل الحدث webhook.test إلى رابط الويب هوك المحدد. يُرسل حتى لو كان الويب هوك موقوفًا، ونجاحه يعيد تشغيله. معرّف غير موجود في مساحة عملك يرد بـ webhook_not_found.
curl -X POST https://derha.net/api/v1/webhooks/WEBHOOK_ID/test \
-H "Authorization: Bearer mh_live_xxxxxxxxxxxxxxxx_your_secret"سجل التسليم
GET/v1/webhooks/deliveries
الأحداث التي أرسلتها ويب هوك مساحة العمل، الأحدث أولًا: الحالة، ورد خادمك، وعدد المحاولات، والمحاولة التالية.
معطيات الاستعلام
| الاسم | النوع | إلزامي | الوصف |
|---|---|---|---|
webhook_id | uuid | لا | ويب هوك واحد فقط. |
status | string | لا | success أو failed أو pending أو queued. |
event | string | لا | نوع حدث، مثل message.received. |
curl "https://derha.net/api/v1/webhooks/deliveries?status=failed" \
-H "Authorization: Bearer mh_live_xxxxxxxxxxxxxxxx_your_secret"إعادة إرسال حدث
POST/v1/webhooks/deliveries/{id}/redeliver
يرسل الحدث مرة أخرى بالجسم نفسه ومعرّف الحدث نفسه، فيعرف خادمك أنه عالجه من قبل. الحدث يحمل الجسم الذي بُني وقت وقوعه، وليس نسخة أحدث. الحد 10 إعادات في الدقيقة لكل ويب هوك.
curl -X POST https://derha.net/api/v1/webhooks/deliveries/DELIVERY_ID/redeliver \
-H "Authorization: Bearer mh_live_xxxxxxxxxxxxxxxx_your_secret"قواعد الإرسال حسب المنصة #
المنصات تختلف في من يحق له بدء الكلام وفي مدة الرد. اقرأ الحقلين can_initiate في القنوات وreply_window في كل رسالة قبل الإرسال.
| الكود | متى يحدث |
|---|---|
cannot_initiate_on_channel | إنستغرام وماسنجر وتيك توك لا تسمح للشركة بالكتابة أولًا. الرد يكون على عميل كتب لك. |
reply_window_closed | انتهت فترة الرد على هذه المحادثة. انتظر حتى يكتب العميل من جديد. |
human_only_window | بعد 24 ساعة على ماسنجر وإنستغرام يرد موظف فقط، وليس الـ API. |
message_limit_reached | بلغت المحادثة حد الرسائل (10 رسائل على تيك توك) حتى يكتب العميل. |
outside_service_window | واتساب الرسمي خارج 24 ساعة يقبل قالبًا معتمدًا فقط. في send-custom تُستعمل الرسالة المرتبطة بقالب، وفي غيرها انتظر العميل. |
text_too_long_for_channel | النص أطول مما تسمح به المنصة. |
media_not_supported_on_channel | المنصة لا تقبل هذا النوع من الملفات. |
media_too_large_for_channel | الملف أكبر مما تسمح به المنصة. |
text_with_media_not_supported | المنصة لا تقبل نصًا مع ملف في رسالة واحدة. أرسلهما رسالتين. |
تنسيق النص
اكتب النص بعلامات واتساب: *غامق* و_مائل_ و~مشطوب~. تيليغرام يستقبلها كتنسيق. إنستغرام وماسنجر وتيك توك يستقبلون الكلمات بلا العلامات. النص المحفوظ في الحقل text يبقى كما أرسلته.
العملاء الذين طلبوا التوقف
عندما يطلب عميل إيقاف الرسائل تُعلَّم هويته على تلك المنصة، وأي إرسال له بالـ API يُرفض بـ 409 والكود contact_opted_out. الرفض خاص بالمنصة: عميل أوقف واتساب يبقى الوصول إليه على تيليغرام ممكنًا. إذا كانت الرسالة عن طلبه أو حجزه أو حسابه (إيصال، موعد، رمز) أرسل "transactional": true. هذا إقرار منك ولا يلغي طلب التوقف. قالب واتساب الرسمي من فئة MARKETING يُرفض دائمًا لعميل أوقف الرسائل.
الويب هوك #
الويب هوك يرسل إلى خادمك طلب POST موقّعًا عند كل حدث: رسالة جديدة من عميل، أو تغيّر حالة رسالة أرسلتها. تعرف ما يحدث فورًا دون استعلام دوري. الويب هوك لا يحتاج مفتاح API.
الإعداد
- في «واجهة API للمطورين» ثم «الويب هوك» أضف رابطًا. يجب أن يكون HTTPS وعامًا ويقبل POST.
- اختر الأحداث ثم القنوات: كلها أو قنوات محددة.
- شغّل روابط تنزيل الملفات إذا كان نظامك سينزّل المرفقات.
- احفظ مفتاح التوقيع، فهو يظهر مرة واحدة، ثم اضغط «اختبار» للتأكد.
الأحداث
تصلك الأحداث التي اشتركت بها فقط. قد تُضاف أنواع جديدة لاحقًا، فتجاهل النوع الذي لا تعرفه بدل أن ترفضه.
| الحدث | متى يُرسل |
|---|---|
message.received | كتب عميل رسالة إلى إحدى قنواتك. يشمل قنوات «API فقط». |
message.recovered | رسالة عميل فاتتك أثناء انقطاع القناة وعُثر عليها بعد عودتها. sent_at يحدد وقت إرسالها. لا تصلك إلا إذا اشتركت بها. |
message.sent | خرجت رسالة صادرة: من المنصة أو من الـ API أو بكتابة صاحب الرقم من هاتفه (source = owner_device). |
message.delivered | وصلت الرسالة إلى هاتف المستلم. |
message.read | قرأ المستلم الرسالة. |
message.failed | تعذّر إرسال الرسالة. السبب في failure_reason. |
webhook.test | حدث تجريبي يُرسل عند الطلب فقط: من لوحة التحكم أو POST /v1/webhooks/{id}/test. |
شكل الحدث
كل حدث كائن JSON فيه id (معرّف الحدث) وtype وapi_version وcreated_at وdata. أحداث الرسائل تحمل data.message بالشكل نفسه للوارد والصادر.
{
"id": "evt_k3j9x2m8q1w7e5r4t6y0u2i8",
"type": "message.received",
"api_version": "v1",
"created_at": "2026-10-03T08:30:00+00:00",
"data": {
"message": {
"id": "01a0ff03-16e0-7350-a174-d09c254ee040",
"direction": "inbound",
"business_id": "9b1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e",
"number_id": "7f2a1c9e-3b4d-5e6f-7a8b-9c0d1e2f3a4b",
"channel": "whatsapp",
"channel_id": "7f2a1c9e-3b4d-5e6f-7a8b-9c0d1e2f3a4b",
"conversation_id": "9c0d1e2f-3a4b-5c6d-7e8f-9a0b1c2d3e4f",
"from": "201014642581",
"to": "201111229562",
"contact": { "id": "5e6f7a8b-9c0d-1e2f-3a4b-5c6d7e8f9a0b", "name": "Ahmed", "phone": "201014642581", "handle": null },
"reply_window": { "state": "open", "closes_at": "2026-10-04T08:29:58+00:00", "remaining_messages": null, "reason": null },
"source": "contact",
"api_key_id": null,
"type": "image",
"text": "Is this one available?",
"attachments": [
{
"id": "01a0ff03-16e0-7350-a174-d09c254ee041",
"type": "image",
"mime_type": "image/jpeg",
"name": null,
"size": 34191,
"ready": true,
"url": "https://derha.net/api/v1/media/01a0ff03-16e0-7350-a174-d09c254ee041"
}
],
"contacts": [],
"reply_to": null,
"status": "received",
"failure_reason": null,
"sent_at": "2026-10-03T08:29:58+00:00",
"created_at": "2026-10-03T08:30:00+00:00"
}
}
}حقول data.message
| الحقل | النوع | المعنى |
|---|---|---|
id | uuid | معرّف الرسالة. |
direction | string | inbound واردة، outbound صادرة. |
business_id | uuid | الشركة التي تتبعها القناة. |
number_id | uuid | القناة التي استقبلتها أو أرسلتها. |
channel | string | المنصة: whatsapp أو messenger أو instagram أو telegram أو tiktok. |
channel_id | uuid | القيمة نفسها كـ number_id. |
conversation_id | uuid | المحادثة التي تتبعها الرسالة. استعملها للرد. |
from | string | المرسل بأرقام دولية بلا +. إن لم يكن للعميل هاتف فمعرّفه على المنصة. |
to | string | المستلم بالصيغة نفسها. |
contact | object | جهة الاتصال: id وname وphone (رقم هاتف فقط) وhandle. |
reply_window | object | هل يجوز الرد الآن: state (open أو human_only أو template_only أو closed) وcloses_at وremaining_messages وreason. |
source | string | من أرسلها: contact للعميل، وapi وagent وcampaign وai_agent وowner_device وغيرها للصادرة. |
api_key_id | uuid|null | المفتاح الذي أُرسلت به رسالة API، وإلا null. يميّز تكاملين لكل منهما مفتاح. |
type | string | text أو image أو video أو audio أو voice أو document أو sticker أو location أو contact_card أو unsupported. |
text | string|null | نص الرسالة أو تعليق الملف. |
attachments | array | الملفات: id وtype وmime_type وsize وready وurl للتنزيل بالمفتاح. مع download_url وdownload_expires_at إذا فعّلت روابط التنزيل. |
contacts | array | بطاقات جهات اتصال شاركها العميل. |
reply_to | uuid|null | معرّف الرسالة التي يرد عليها العميل. |
status | string | received للواردة، أو حالة الصادرة. |
failure_reason | string|null | سبب الفشل في message.failed. |
sent_at | ISO 8601 | وقت إرسال العميل لها. أبكر من created_at للرسالة المسترجعة أو المتأخرة. |
created_at | ISO 8601 | وقت تسجيلها في المنصة. |
التحقق من التوقيع
تحقق من كل طلب قبل الوثوق به. الترويسة X-MessageHub-Signature-256 بالشكل t=<الوقت>,v1=<التوقيع>، والتوقيع هو HMAC-SHA256 للنص "الوقت.الجسم" بمفتاح التوقيع، مكتوبًا بأرقام ست عشرية. ارفض الطلب إذا بعد الوقت عن ساعتك أكثر من 5 دقائق، فهذا يمنع إعادة تشغيل طلب قديم. استعمل الجسم كما وصل بالضبط دون إعادة تسلسله.
| الترويسة | القيمة |
|---|---|
X-MessageHub-Event | نوع الحدث. |
X-MessageHub-Delivery | معرّف الحدث، وهو id في الجسم. استعمله لتجاهل التكرار. |
X-MessageHub-Webhook-Id | معرّف الويب هوك المرسل. مفيد إذا شاركت عدة ويب هوك رابطًا واحدًا. |
X-MessageHub-Timestamp | وقت الإرسال بالثواني (Unix). |
X-MessageHub-Signature-256 | التوقيع الموصى به: t=…,v1=…. قد يحمل عدة قيم v1 أثناء تغيير المفتاح. |
X-MessageHub-Signature | التوقيع الأصلي sha256=… للجسم وحده. للمستقبِلات القديمة، والأفضل ترويسة 256. |
User-Agent | MessageHub-Webhooks/1.0 |
import crypto from 'node:crypto'
// Read the RAW body: app.post('/webhook', express.raw({ type: 'application/json' }), handler)
export function verifyMessageHub(rawBody, header, secret, toleranceSeconds = 300) {
const parts = header.split(',').map((p) => p.split('='))
const t = parts.find(([k]) => k === 't')?.[1]
const signatures = parts.filter(([k]) => k === 'v1').map(([, v]) => v)
if (!t || Math.abs(Date.now() / 1000 - Number(t)) > toleranceSeconds) return false
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
return signatures.some(
(sig) => sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)),
)
}
// verifyMessageHub(req.body.toString('utf8'), req.get('X-MessageHub-Signature-256'), process.env.MESSAGEHUB_WEBHOOK_SECRET)<?php
function verifyMessageHub(string $rawBody, string $header, string $secret, int $tolerance = 300): bool
{
$t = null;
$signatures = [];
foreach (explode(',', $header) as $part) {
[$k, $v] = array_pad(explode('=', $part, 2), 2, '');
if ($k === 't') { $t = $v; } elseif ($k === 'v1') { $signatures[] = $v; }
}
if ($t === null || abs(time() - (int) $t) > $tolerance) {
return false;
}
$expected = hash_hmac('sha256', $t.'.'.$rawBody, $secret);
foreach ($signatures as $signature) {
if (hash_equals($expected, $signature)) {
return true;
}
}
return false;
}
$raw = file_get_contents('php://input');
if (! verifyMessageHub($raw, $_SERVER['HTTP_X_MESSAGEHUB_SIGNATURE_256'] ?? '', getenv('MESSAGEHUB_WEBHOOK_SECRET'))) {
http_response_code(401);
exit;
}
http_response_code(200); // answer first, then process the event in a queueimport hashlib, hmac, time
def verify_messagehub(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
t, signatures = None, []
for part in header.split(','):
key, _, value = part.partition('=')
if key == 't':
t = value
elif key == 'v1':
signatures.append(value)
if t is None or abs(time.time() - int(t)) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected, s) for s in signatures)
# Flask: verify_messagehub(request.get_data(), request.headers.get('X-MessageHub-Signature-256', ''), SECRET)تغيير مفتاح التوقيع
عند إنشاء مفتاح توقيع جديد يبقى القديم صالحًا 24 ساعة، ويحمل كل طلب توقيعين v1، واحدًا لكل مفتاح. حدّث خادمك خلال هذه المدة فلا يضيع أي حدث.
التسليم وإعادة المحاولة
- يُعد الحدث مسلَّمًا عندما يرد خادمك بأي 2xx خلال 5 ثوانٍ.
- الرد 5xx أو 429 أو خطأ شبكة: تُعاد المحاولة، مع احترام
Retry-Afterإن أرسلته. - أي 4xx آخر أو تحويل (redirect): فشل نهائي دون إعادة. لا تُتبع التحويلات.
- المحاولات بعد 10 ثوانٍ ثم دقيقة ثم 5 دقائق ثم 30 دقيقة ثم ساعة ثم 3 ساعات ثم 6 ساعات.
- بعد 20 فشلًا نهائيًا متتاليًا يُوقف الويب هوك ويُنبَّه مالك مساحة العمل.
- حدث تجريبي ناجح يعيد تشغيل الويب هوك الموقوف. ويمكن إعادة إرسال أي حدث من السجل.
الاستقبال الجيد
- رد بـ 200 بسرعة، ثم عالج الحدث في الخلفية.
- قد يصل الحدث نفسه أكثر من مرة. احفظ معرّفه وتجاهل التكرار.
- ترتيب الوصول غير مضمون. اعتمد على
created_atوعلى ترتيب الحالات: sent ثم delivered ثم read. - تحقق من التوقيع والوقت قبل أي شيء آخر.
- نزّل الملفات من url بمفتاحك، وأعد المحاولة ما دام ready=false.
- إذا توقف خادمك فعوّض ما فاتك بـ
GET /v1/messages/inbound، أو أعد إرسال الأحداث الفاشلة من السجل.
مثال كامل: مستقبِل بـ Node.js
import express from 'express'
import { verifyMessageHub } from './verify.js'
const app = express()
const seen = new Set() // use Redis or a database table in production
app.post('/messagehub', express.raw({ type: 'application/json' }), (req, res) => {
const raw = req.body.toString('utf8')
if (!verifyMessageHub(raw, req.get('X-MessageHub-Signature-256') ?? '', process.env.MESSAGEHUB_WEBHOOK_SECRET)) {
return res.sendStatus(401)
}
res.sendStatus(200) // answer first, within 5 seconds
const event = JSON.parse(raw)
if (seen.has(event.id)) return // the same event can arrive twice
seen.add(event.id)
if (event.type === 'message.received') {
const m = event.data.message
console.log('New message from', m.from, ':', m.text)
}
})
app.listen(3000)روابط تنزيل الملفات
الرابط url في المرفق يحتاج مفتاح API. إذا كان نظامك يستقبل الويب هوك دون مفتاح فشغّل «روابط تنزيل الملفات» للويب هوك، فيحمل كل مرفق download_url يُفتح دون مفتاح حتى download_expires_at (24 ساعة افتراضيًا). الرابط الموقّع لا يمكن إلغاؤه، فمن يحصل عليه يفتح الملف حتى ينتهي. لذلك هو مغلق افتراضيًا.
الأخطاء #
كل خطأ يعيد الغلاف نفسه مع code ثابت يمكنك البناء عليه. الرمز HTTP وحده لا يكفي، فعدة أكواد تشترك في الرمز نفسه (مثل 404 و422). ابنِ منطقك على code. أخطاء التحقق تضيف كائن errors بتفاصيل كل حقل.
| الكود | HTTP | المعنى |
|---|---|---|
unauthenticated | 401 | مفتاح API غير صالح أو غير موجود. |
origin_not_allowed | 401 | الطلب من أصل غير مسموح لهذا المفتاح. |
insecure_transport | 403 | يجب استدعاء الـ API عبر HTTPS. |
ip_not_allowed | 403 | الطلب من عنوان IP غير مسموح لهذا المفتاح. |
business_not_found | 404 | لا توجد شركة بهذا المعرّف في مساحة عملك. |
number_not_found | 404 | لا توجد قناة بهذا number_id في مساحة عملك. |
conversation_not_found | 404 | لا توجد محادثة بهذا المعرّف في مساحة عملك. |
contact_not_found | 404 | لا توجد جهة اتصال بهذا المعرّف. |
media_not_found | 404 | لا يوجد ملف بهذا المعرّف يحق لمفتاحك فتحه. |
webhook_not_found | 404 | لا يوجد ويب هوك بهذا المعرّف في مساحة عملك. |
message_not_found | 404 | لا توجد رسالة بهذا المعرّف. |
custom_message_not_found | 404 | الرسالة المخصصة المطلوبة غير موجودة في مساحة عملك. |
number_not_connected | 409 | القناة المختارة غير متصلة الآن. |
media_not_ready | 409 | لم يُجلب الملف من المنصة بعد. أعد المحاولة بعد 30 ثانية. |
request_in_progress | 409 | طلب بالـ Idempotency-Key نفسه ما زال قيد المعالجة. |
contact_opted_out | 409 | طلب العميل التوقف على هذه المنصة. أرسل transactional=true فقط إذا كانت الرسالة عن طلبه. |
validation_failed | 422 | جسم الطلب غير صالح. |
slot_value_missing | 422 | قيمة ناقصة لخانة إلزامية في الرسالة المخصصة. |
contact_not_on_channel | 422 | جهة الاتصال ليس لها حساب على منصة هذه القناة. |
cannot_initiate_on_channel | 422 | هذه المنصة لا تسمح للشركة بالكتابة أولًا. |
reply_window_closed | 422 | انتهت فترة الرد على هذه المحادثة. |
human_only_window | 422 | الرد الآن للموظفين فقط وليس للـ API. |
message_limit_reached | 422 | بلغت المحادثة حد الرسائل حتى يكتب العميل. |
outside_service_window | 422 | واتساب الرسمي خارج 24 ساعة يقبل قالبًا معتمدًا فقط. |
text_too_long_for_channel | 422 | النص أطول مما تسمح به المنصة. |
media_not_supported_on_channel | 422 | المنصة لا تقبل هذا النوع من الملفات. |
media_too_large_for_channel | 422 | الملف أكبر مما تسمح به المنصة. |
text_with_media_not_supported | 422 | المنصة لا تقبل نصًا مع ملف. أرسلهما رسالتين. |
idempotency_key_reused | 422 | استُعمل هذا الـ Idempotency-Key لطلب بجسم مختلف. |
quota_exhausted | 422 | رصيد رسائل API في مساحة العمل انتهى. اشحنه لتتابع. |
api_not_enabled | 422 | الـ API غير مفعّل لمساحة العمل. اشترِ رصيد رسائل API لتفعيله. |
rate_limited | 429 | طلبات كثيرة. انتظر Retry-After ثم أعد المحاولة. |
server_error | 500 | حدث خطأ غير متوقع. أعد المحاولة. |
تُعاد المحاولة بأمان مع 429 و5xx (ومع Idempotency-Key لا يتكرر الإرسال). أخطاء 4xx الأخرى سببها الطلب نفسه، فصحّحه ولا تكرره كما هو.
أسئلة شائعة #
كيف أحصل على مفتاح API؟
من لوحة التحكم افتح «واجهة API للمطورين» ثم «المفاتيح» وأنشئ مفتاحًا. يظهر الرمز مرة واحدة فقط، فانسخه فورًا. ويلزم رصيد رسائل API في مساحة العمل.
هل الإرسال فوري؟
لا. نقطة الإرسال ترد 202 فورًا بعد وضع الرسالة في الطابور، ويتم الإرسال الفعلي بعدها. للنتيجة النهائية استعمل «حالة رسالة» أو الويب هوك.
لماذا لا تظهر رسائل الـ API في صندوق الوارد؟
لأن هذا المسار مخصص للإشعارات ورموز التحقق والإيصالات، وليس محادثة بين موظف وعميل. تابعها بنقطة الحالة أو بالويب هوك.
كيف أمنع إرسال الرسالة مرتين عند إعادة المحاولة؟
أرسل ترويسة Idempotency-Key بقيمة فريدة لكل عملية، مثل رقم الطلب. الطلب المعاد بالقيمة نفسها يعيد النتيجة الأولى.
هل أستطيع الكتابة لأي عميل أولًا؟
يعتمد على المنصة. واتساب يسمح حسب نوع القناة، أما إنستغرام وماسنجر وتيك توك فلا تسمح للشركة بالكتابة أولًا. اقرأ can_initiate في GET /v1/numbers.
كيف أعرف أن رسالة وصلت؟
اشترك بالأحداث message.delivered وmessage.read في الويب هوك، أو اقرأ status في نقطة «حالة رسالة».
هل يمكنني إنشاء الويب هوك من الكود؟
لا. الإنشاء والتعديل من لوحة التحكم فقط. من الكود تستطيع عرضها واختبارها وقراءة سجلها وإعادة إرسال أحداثها.
هل يمكن إرسال صور أو ملفات بالـ API؟
ليس الآن. نقطة الإرسال تدعم النص (type=text)، والرسائل المخصصة تغطي الرسائل الجاهزة. استقبال الملفات من العملاء وتنزيلها مدعوم.