مرجع واجهة مسار

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

English

واجهة REST واحدة فوق المحرّك نفسه الذي تستخدمه لوحة التحكم. لا يوجد مسار ثانٍ إلى المستلم: الخصم والحصة ومنع التكرار وحجز الإرسال متطابقة سواء أُرسلت الرسالة من شاشة أو من كودك.

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

المصادقة

أرسل مفتاحك كرمز Bearer في كل طلب. المفتاح هو ما يحدّد الحساب — لا يقبل أي مسار معرّف حساب، وأي عنصر يخص حساباً آخر يجيب 404.

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

المفتاح يبدأ بـ msr_live_ ثم 48 حرفاً ثم 6 أحرف تدقيق. شرطات سفلية لا وسطية: الشرطة السفلية ليست حرفاً في Base64، فلا يمكن أن يُخطئ أحد بين بصمة عشوائية ومفتاح حقيقي.

يُعرض المفتاح مرة واحدة عند الإنشاء ويُحفظ كبصمة SHA-256 فقط. لا يمكن استرجاعه — إن فُقد فأبطِله وأنشئ غيره.

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

الصلاحيات

كل مسار يطلب صلاحية واحدة. امنح المفتاح ما يحتاجه تكامله فقط — مفتاح للتقارير بلا 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}
messages:send /v1/messages
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

الأخطاء

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

{
  "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 هذا المفتاح لا يحمل الصلاحية التي يحتاجها هذا الطلب.
account_not_approved 403 هذا الحساب غير معتمد للإرسال بعد. يفتح الإرسال فور اعتماد الحساب.
quota_exceeded 403 استهلك هذا الحساب حصة باقته في الفترة الحالية.
not_found 404 لا يوجد عنصر بهذا المعرّف.
method_not_allowed 405 هذه الطريقة غير مسموحة على هذا المسار.
idempotency_in_flight 409 ما زال هناك طلب بنفس المفتاح قيد المعالجة. أعد المحاولة بعد لحظة.
channel_unusable 409 هذا الرقم لا يستطيع الإرسال الآن — قد يكون غير متصل، أو محدوداً من ميتا، أو بلا رمز وصول.
template_not_usable 409 حالة هذا القالب لا تسمح بهذا الإجراء.
campaign_state_invalid 409 هذه الحملة ليست في حالة تسمح بهذا الإجراء. يحمل الرفض حالتها الحالية.
campaign_not_launchable 409 تعذّر تشغيل هذه الحملة. يحمل الرفض رمز سبب يحدّد أي شرط لم يتحقق.
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 لا يمكن الإرسال إلى هذا المستلم.
webhook_url_refused 422 تم رفض رابط الويب هوك.
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 في الدقيقة

منع التكرار

كل طلب كتابة ينفق مالاً أو لا يمكن التراجع عنه يحتاج ترويسة 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/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 مع الرسالة في الطابور؛ ويصلك التسليم عبر الويب هوك أو بقراءة الرسالة.

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 يعني «خُصم ودخل الطابور» لا «سُلّم». يُخصم الرصيد قبل الإرسال، لأن خصماً لرسالة لم تخرج يُصلحه استرداد، أما رسالة خرجت ولم تُخصم على أحد فلا يصلحها شيء.

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');

الويب هوك

نرسل الأحداث موقّعة إلى رابطك عبر 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` فلا يتغيّر عبر السلّم — وهو ما تعتمد عليه في منع التكرار، لا الطابع الزمني.

النقطة التي تفشل 20 تسليمات متتالية تُوقَف تلقائياً. والرمز 410 Gone يوقفها فوراً — نأخذ بكلامك.

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

الأحداث

كل حدث يُرسَل مرة واحدة لكل واقعة. انتقال الحالة من «أُرسِلت» إلى «وصلت» إلى «قُرِئت» ثلاث تسليمات، والإشعار الذي يكرّره المزوّد يبقى واحداً.

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

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