دليل واجهة WhatsApp API وWebhooks للمطورين | مسار

أرسل قوالب واتساب من نظامك أنت.

استخدم واجهة REST لإرسال الرسائل وإدارة البيانات من نظامك. تطبق الواجهة قواعد الحصص ورسوم الإدارة ومنع التكرار نفسها الموجودة في لوحة التحكم.

العنوان الأساسي: https://router.atyaf.co/api/v1

المصادقة

أرسل مفتاح API كرمز Bearer في كل طلب. يحدد المفتاح الحساب تلقائياً، لذلك لا ترسل معرّف الحساب. أي عنصر يخص حساباً آخر يعيد 404.

curl https://router.atyaf.co/api/v1/me \
  -H "Authorization: Bearer msr_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

يبدأ مفتاح الإنتاج بـ msr_live_، ثم 48 حرفاً، ثم رمز تحقق من 6 أحرف. انسخه كاملاً كما يظهر عند الإنشاء.

يظهر المفتاح مرة واحدة عند الإنشاء، ولا يمكن استرجاعه لاحقاً. إذا فقدته، ألغِه وأنشئ مفتاحاً جديداً.

يمكن تقييد المفتاح بقائمة عناوين وتحديد تاريخ انتهاء له. كلاهما يُرفض عند المصادقة، قبل أي خصم.

الصلاحيات

كل مسار يطلب صلاحية واحدة. امنح المفتاح ما يحتاجه التكامل فقط. مفتاح التقارير الذي لا يملك صلاحية messages:send لا يستطيع الإرسال أو الإنفاق، لكنه قد يكشف بيانات حسابك. أبطل أي مفتاح يتسرّب فوراً وأنشئ بديلاً.

الصلاحية تسمح بـ
channels:read /v1/channels · /v1/channels/{channel}
templates:read /v1/templates · /v1/templates/{template}
templates:write /v1/templates · /v1/templates/{template}/submit
contacts:read /v1/contacts · /v1/contacts/{contact} · /v1/contact-imports · /v1/contact-imports/{import} · /v1/lists · /v1/lists/{list}
contacts:write /v1/contacts · /v1/contacts/{contact} · /v1/contact-imports · /v1/lists · /v1/lists/{list}/contacts · /v1/lists/{list}/contacts/{contact} · /v1/lists/{list}
messages:read /v1/messages · /v1/messages/{message} · /v1/messages/{message}/image · /v1/media/{mediaId}
messages:send /v1/messages · /v1/media
campaigns:read /v1/campaigns · /v1/campaigns/{campaign} · /v1/campaigns/{campaign}/report
campaigns:write /v1/campaigns · /v1/campaigns/{campaign}/start · /v1/campaigns/{campaign}/pause · /v1/campaigns/{campaign}/resume · /v1/campaigns/{campaign}/cancel
wallet:read /v1/balance · /v1/rates
webhooks:write /v1/webhooks · /v1/webhooks/{endpoint} · /v1/webhooks/{endpoint}/rotate-secret · /v1/webhooks/{endpoint}/retire-secret · /v1/webhooks/{endpoint}/test

الأخطاء

تعيد كل استجابة فاشلة رمز خطأ ثابتاً داخل البنية نفسها. ابنِ منطق التكامل على الرمز، لا على نص الرسالة، لأن النص مترجم وقد يتغير.

{
  "error": {
    "code": "scope_missing",
    "message": "…",
    "request_id": "req_1a2b3c…",
    "required_scope": "messages:send"
  }
}

كل استجابة تحمل ترويسة Masar-Request-Id، وتتكرّر داخل جسم الخطأ. اذكرها في طلب الدعم فتقودنا إلى طلب واحد بعينه في سجلّك.

الرمز HTTP المعنى
auth_missing 401 لم يصل أي مفتاح. أرسل مفتاحك في ترويسة Authorization بالشكل: `Authorization: Bearer msr_live_…`.
auth_invalid 401 هذا المفتاح غير صالح. تأكد أنه لم يُقتطع أو يُكتب خطأً.
auth_revoked 401 تم إبطال هذا المفتاح. أنشئ مفتاحاً جديداً من لوحة التحكم في قسم المطوّرين.
auth_expired 401 انتهت صلاحية هذا المفتاح. أنشئ مفتاحاً جديداً من لوحة التحكم في قسم المطوّرين.
auth_ip_not_allowed 403 هذا المفتاح مقيّد بقائمة عناوين محدّدة، وهذا الطلب لم يأتِ من أحدها.
test_key_cannot_spend 403 هذا مفتاح اختبار، ولا يسمح بطلب ينفق من الرصيد أو يسوّي مبلغاً محجوزاً. لم يُرسل أو يُخصم أو يُسوَّ شيء، ولم تتغير أي حملة. استخدم مفتاح إنتاج من قسم المطوّرين عندما تريد إرسالاً فعلياً.
account_unavailable 403 الحساب المرتبط بهذا المفتاح غير متاح. افتح لوحة التحكم للتحقق من حالة الحساب.
plan_feature_missing 403 واجهة الـ API غير مشمولة في باقة هذا الحساب. رقِّ الباقة لتفعيلها.
scope_missing 403 هذا المفتاح لا يملك الصلاحية المطلوبة. راجع حقل `required_scope`، ثم أنشئ مفتاحاً جديداً بالصلاحية المناسبة أو عدّل إعداد التكامل.
account_not_approved 403 الحساب بانتظار اعتماد فريق مسار. يمكنك تجهيز التكامل الآن، ويبدأ الإرسال بعد الاعتماد.
quota_exceeded 403 استهلك الحساب حصة الباقة للفترة الحالية. انتظر تجدد الحصة أو اطلب ترقية الباقة من لوحة التحكم.
not_found 404 لا يوجد عنصر بهذا المعرّف.
method_not_allowed 405 هذه الطريقة غير مسموحة على هذا المسار.
idempotency_in_flight 409 ما زال هناك طلب بنفس المفتاح قيد المعالجة. أعد المحاولة بعد لحظة.
channel_unusable 409 رقم الإرسال غير جاهز الآن. افتح صفحة الأرقام لمعرفة الحالة والإجراء المطلوب، ثم أعد المحاولة.
service_window_closed 409 النص الحرّ مسموح فقط خلال 24 ساعة من آخر رسالة أرسلها لك هذا الشخص، وقد انتهت هذه المهلة. أرسل قالباً معتمداً بدلاً من ذلك، أو انتظر حتى يكتب إليك مجدداً. ولم يُخصم شيء.
template_not_usable 409 هذا القالب غير جاهز للإرسال. راجع حالته في صفحة القوالب واختر قالباً معتمداً.
campaign_state_invalid 409 حالة الحملة الحالية لا تسمح بهذا الإجراء. راجع حقل `current_status` قبل إرسال طلب جديد.
campaign_not_launchable 409 تعذّر بدء الحملة. راجع حقل `reason` لمعرفة الشرط المطلوب، ثم صححه وأعد المحاولة.
cost_refused 409 تجاوزت رسوم الجمهور الحد `max_cost_microusd`، لذلك لم يُحجز أي مبلغ. راجع التكلفة الفعلية في الاستجابة ثم أعد الطلب بحد مناسب.
resource_in_use 409 هذه القائمة هي جمهور حملة لم تنتهِ بعد. ألغِ الحملة أو انتظر انتهاءها قبل الحذف.
validation_failed 422 تعذّر قبول الطلب بالشكل الذي أُرسل به.
confirmation_required 422 هذا الطلب ينفق مالاً أو لا يمكن التراجع عنه، لذلك يحتاج "confirm": true في الجسم.
idempotency_conflict 422 استُخدم هذا المفتاح (Idempotency-Key) من قبل لرسالة مختلفة. استخدم مفتاحاً جديداً لكل رسالة مختلفة.
recipient_unsendable 422 لا يمكن الإرسال إلى هذا المستلم. راجع حقل `reason` لمعرفة السبب قبل إعادة المحاولة.
webhook_url_refused 422 تعذّر قبول رابط Webhook. راجع حقل `reason` وصحح الرابط قبل إعادة المحاولة.
file_unreadable 422 تعذّرت قراءة الملف المرفوع كملف CSV.
insufficient_balance 402 رصيد رسوم إدارة مسار لا يكفي لهذه الرسالة. أضف رصيداً ثم أعد المحاولة.
rate_limited 429 عدد الطلبات على هذا المفتاح تجاوز الحد. انتظر المدة المذكورة في ترويسة Retry-After.
pricing_unavailable 500 لا يوجد سعر مُعرَّف لهذه الرسالة. المشكلة من طرفنا؛ يرجى التواصل مع الدعم.
server_error 500 حدث خطأ من طرفنا. اذكر معرّف الطلب (request id) عند التواصل مع الدعم.

حدود المعدّل

الحدود لكل مفتاح ولكل دقيقة، لا لكل عنوان IP — عميلان خلف اتصال مكتب واحد لا يتشاركان الحصة. كل استجابة تحمل X-RateLimit-Limit وX-RateLimit-Remaining، والرفض يحمل Retry-After.

القراءة والكتابة عدا الإرسال 600 في الدقيقة
الإرسال · POST /v1/messages 120 في الدقيقة
تنزيل المرفقات · GET /v1/media/{id} 240 في الدقيقة
جلسات MCP للمساعدات 240 في الدقيقة

منع التكرار

كل طلب كتابة ينفق مالاً أو لا يمكن التراجع عنه يحتاج ترويسة Idempotency-Key: ‏POST /v1/messages و POST /v1/campaigns وبدء الحملة وإيقافها واستئنافها وإلغاؤها. أي نص فريد يكفي، وUUID لكل طلب هو الخيار المعتاد.

تمنع هذه الترويسة تكرار الإرسال أو الحجز عندما يعيد نظامك الطلب بسبب انقطاع الاتصال.

تُؤخذ بصمة المحتوى على الرقم بعد التوحيد، فإعادة المحاولة بصيغة رقم مختلفة تُعيد الجواب بدل أن تتعارض.

المسارات

Method Path الصلاحية
GET /v1/me —
GET /v1/balance wallet:read
GET /v1/rates wallet:read
GET /v1/channels channels:read
GET /v1/channels/{channel} channels:read
GET /v1/templates templates:read
POST /v1/templates templates:write
GET /v1/templates/{template} templates:read
POST /v1/templates/{template}/submit templates:write
GET /v1/messages messages:read
POST /v1/messages messages:send
GET /v1/messages/{message} messages:read
GET /v1/messages/{message}/image messages:read
GET /v1/media/{mediaId} messages:read
POST /v1/media messages:send
GET /v1/webhooks webhooks:write
POST /v1/webhooks webhooks:write
PATCH /v1/webhooks/{endpoint} webhooks:write
DELETE /v1/webhooks/{endpoint} webhooks:write
POST /v1/webhooks/{endpoint}/rotate-secret webhooks:write
POST /v1/webhooks/{endpoint}/retire-secret webhooks:write
POST /v1/webhooks/{endpoint}/test webhooks:write
GET /v1/contacts contacts:read
POST /v1/contacts contacts:write
GET /v1/contacts/{contact} contacts:read
DELETE /v1/contacts/{contact} contacts:write
GET /v1/contact-imports contacts:read
POST /v1/contact-imports contacts:write
GET /v1/contact-imports/{import} contacts:read
GET /v1/lists contacts:read
POST /v1/lists contacts:write
GET /v1/lists/{list} contacts:read
POST /v1/lists/{list}/contacts contacts:write
DELETE /v1/lists/{list}/contacts/{contact} contacts:write
DELETE /v1/lists/{list} contacts:write
GET /v1/campaigns campaigns:read
POST /v1/campaigns campaigns:write
GET /v1/campaigns/{campaign} campaigns:read
GET /v1/campaigns/{campaign}/report campaigns:read
POST /v1/campaigns/{campaign}/start campaigns:write
POST /v1/campaigns/{campaign}/pause campaigns:write
POST /v1/campaigns/{campaign}/resume campaigns:write
POST /v1/campaigns/{campaign}/cancel campaigns:write

مواصفة OpenAPI

هذه الواجهة كاملة منشورة كمستند OpenAPI 3.1 على رابط ثابت. وجّه أي مولّد إليه لتحصل على عميل مكتوب بلغتك بدل عميل يدوي يتخلّف عن الواجهة مع الوقت.

https://router.atyaf.co/docs/openapi.json

يُعاد توليد المستند من المسارات المتاحة مع كل إصدار. تستخدم كل العمليات مصادقة Bearer، ولا تحتاج إلى إرسال معرّف الحساب.

إرسال رسالة

أرسل قالباً معتمداً إلى مستلم واحد. تعني الاستجابة 202 أن الرسالة دخلت الطابور؛ وتصل تحديثات التسليم عبر Webhook أو عند قراءة بيانات الرسالة.

curl

curl -X POST https://router.atyaf.co/api/v1/messages \
  -H "Authorization: Bearer $MASAR_KEY" \
  -H "Idempotency-Key: order-1234" \
  -H "Content-Type: application/json" \
  -d '{
        "to": "+970599123456",
        "template_id": 12,
        "variables": ["Ahmad", "1234"]
      }'
HTTP/1.1 202 Accepted
Masar-Request-Id: req_1a2b3c…
X-RateLimit-Remaining: 119

{
  "data": {
    "id": 8417,
    "to": "+970599123456",
    "status": "queued",
    "dispatch_state": "pending",
    "price_microusd": 18100,
    "price": "0.0181"
  }
}

تعني الاستجابة 202 أن مسار قبل الرسالة وأضافها إلى قائمة الإرسال. لا تعني أنها وصلت. تظهر النتيجة النهائية في حالة الرسالة أو عبر Webhook، وتُعاد رسوم الإدارة تلقائياً إذا لم تُرسل.

الردّ داخل محادثة

أرسل `text` بدل `template_id` للرد على شخص كتب إليك. لا تحتاج إلى قالب، ولا يخصم مسار رسوم إدارة لهذا الرد. تظهر أي تكلفة لدى ميتا في حساب أعمالك.

curl -X POST https://router.atyaf.co/api/v1/messages   -H "Authorization: Bearer $MASAR_KEY"   -H "Idempotency-Key: reply-8417"   -H "Content-Type: application/json"   -d '{
    "to": "+970599123456",
    "text": "Your order is on its way."
  }'

خلال 24 ساعة من آخر رسالة أرسلها لك ذلك الشخص فقط. وخارجها يُرفض الطلب بـ`service_window_closed` قبل أن يُخصم أي شيء، والقالب المعتمد هو الطريق.

الرسالة التي تصل ومعها صورة أو مستند تحمل معرّف وسائط. اجلب بياناتها عبر: GET https://router.atyaf.co/api/v1/media/{id}

PHP

<?php

$response = Http::withToken($key)
    ->withHeaders(['Idempotency-Key' => 'order-1234'])
    ->post('https://router.atyaf.co/api/v1/messages', [
        'to' => '+970599123456',
        'template_id' => 12,
        'variables' => ['Ahmad', '1234'],
    ]);

if ($response->header('Idempotent-Replayed') === 'true') {
    // This retry sent nothing and charged nothing. The body is the
    // original call's answer.
}

$messageId = $response->json('data.id');

Webhook

نرسل الأحداث موقّعة إلى رابطك عبر POST. التسليمات تتبع Standard Webhooks، فأي مكتبة متوافقة تتحقّق منها دون كود خاص.

ثلاث ترويسات ترافق كل تسليم:

webhook-idmsg_…
webhook-timestamp1767225600
webhook-signaturev1,<base64> v1,<base64>

التوقيع هو HMAC-SHA256 للمعرّف والطابع الزمني والجسم الخام، موصولة بنقاط. تحقّق من البايتات كما وصلتك، لا من كائن أعدت ترميزه.

signed = webhook-id + "." + webhook-timestamp + "." + raw_body
signature = base64(hmac_sha256(signed, base64_decode(secret_without_prefix)))

ارفض أي تسليم يبعد طابعه الزمني أكثر من 300 ثانية عن ساعتك. التوقيع يبقى صالحاً إلى الأبد بعد التقاطه، وهذه النافذة هي ما يمنع إعادة إرساله إليك لاحقاً.

يمكن أن يكون مفتاحا توقيع فعّالان معاً أثناء التبديل. أضف المفتاح الجديد، وتأكد من نجاح التحقق، ثم ألغِ القديم.

التحقق من تسليم

<?php

/**
 * Verify one Masar delivery.
 *
 * $body MUST be the raw request body, exactly as received. Decoding it to
 * an array and re-encoding produces different bytes — a space, a unicode
 * escape, a key order — and the signature will never match again.
 */
function masar_webhook_is_valid(string $body, array $headers, array $secrets): bool
{
    $id        = $headers['webhook-id'] ?? '';
    $timestamp = (int) ($headers['webhook-timestamp'] ?? 0);
    $offered   = $headers['webhook-signature'] ?? '';

    // Replay window first. A signature stays valid forever once captured.
    if ($timestamp <= 0 || abs(time() - $timestamp) > 300) {
        return false;
    }

    $signed = $id . '.' . $timestamp . '.' . $body;

    foreach (preg_split('/\s+/', trim($offered)) as $candidate) {
        foreach ($secrets as $secret) {
            $key = base64_decode(substr($secret, strlen('whsec_')), true) ?: $secret;
            $mine = 'v1,' . base64_encode(hash_hmac('sha256', $signed, $key, true));

            // hash_equals, never ===: a comparison that short-circuits leaks
            // the position of the first wrong byte.
            if (hash_equals($mine, $candidate)) {
                return true;
            }
        }
    }

    return false;
}

تُعاد محاولة التسليم الفاشل بعد الفترات التالية، بالثواني: 10, 60, 300, 1800, 7200, 21600

تُوقّع كل محاولة عند إرسالها بطابع زمني جديد. يبقى `webhook-id` ثابتاً في جميع المحاولات، فاستخدمه لمنع معالجة الحدث مرتين.

{1}يتوقف رابط Webhook تلقائياً بعد محاولة فاشلة واحدة، ويتوقف فوراً عند استجابة 410 Gone.|{2}يتوقف رابط Webhook تلقائياً بعد محاولتين فاشلتين متتاليتين، ويتوقف فوراً عند استجابة 410 Gone.|[3,10]يتوقف رابط Webhook تلقائياً بعد 20 محاولات فاشلة متتالية، ويتوقف فوراً عند استجابة 410 Gone.|[11,*]يتوقف رابط Webhook تلقائياً بعد 20 محاولةً فاشلةً متتاليةً، ويتوقف فوراً عند استجابة 410 Gone.

لا نتبع إعادة التوجيه ونعدّها فشلاً. يجب أن يكون الرابط http أو https على المنفذ 80 أو 443، بلا اسم مستخدم أو كلمة مرور، وأن يشير إلى عنوان عام؛ ونعيد فحصه لحظة التسليم لأن الـ DNS متغيّر.

الأحداث

نرسل حدثاً واحداً لكل تغيير. انتقال الرسالة من «أُرسلت» إلى «وصلت» ثم «قُرئت» ينتج ثلاثة أحداث منفصلة.

الحدث متى يُرسَل
message.accepted قبل مسار الرسالة وأضافها إلى قائمة الإرسال، وخُصمت رسوم الإدارة المرتبطة بها إن وجدت. لا يعني هذا الحدث أنها وصلت.
message.sent سُلِّمت إلى واتساب وقبِلها. يتوفر معرّف الرسالة لدى ميتا من هذه اللحظة.
message.delivered أقرّ جهاز المستلم باستلامها.
message.read فتحها المستلم. فقط لمن يفعّل إشعارات القراءة.
message.failed لن تصل الرسالة. تُعاد رسوم إدارة مسار التي خُصمت عنها إن وجدت. يصدر الحدث عند الرفض الفوري أو الفشل اللاحق أو توقف المحاولات.
message.received أرسل أحدهم رسالة إلى أحد أرقامك. يحمل الجسم من أرسلها، والنص إن وُجد، ومعرّف وسائط تجلبه عبر مسار الوسائط. والاستقبال لا يكلّف شيئاً، فلا سعر عليه.
template.approved صار القالب قابلاً للإرسال. تُرسَل سواء وصل الحكم عبر ويب هوك من ميتا أو اكتشفته مطابقة الفهرس عندنا.
template.rejected رُفض القالب، وتتضمن البيانات السبب والجهة التي اتخذت القرار: فريق مسار أو ميتا.
channel.quality_changed تغيّر تقييم الجودة أو مستوى الإرسال الذي تحتفظ به ميتا لأحد أرقامك. الجسم يصف الرقم كما هو الآن.
wallet.low_balance انخفض الرصيد عن الحد الذي حددته، أو عن إنفاق الأسبوع الماضي. يتضمن الإشعار التاريخ المتوقع لنفاد الرصيد، ويُرسل مرة واحدة يومياً على الأكثر.

أنشئ مفتاحاً من لوحة التحكم