استخدم واجهة 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 لكل طلب هو الخيار المعتاد.
تمنع هذه الترويسة تكرار الإرسال أو الحجز عندما يعيد نظامك الطلب بسبب انقطاع الاتصال.
- المفتاح نفسه بالمحتوى نفسه: يُعاد جواب الطلب الأول مع الترويسة Idempotent-Replayed: true، ولا يُرسل ولا يُخصم شيء ثانية.
- إذا استخدمت المفتاح نفسه مع محتوى مختلف، تعيد الواجهة 422 ورمز idempotency_conflict. استخدم مفتاحاً جديداً لكل عملية مختلفة.
- المفتاح نفسه والطلب الأول ما زال يُعالَج: 409 idempotency_in_flight. أعد المحاولة بعد لحظة.
تُؤخذ بصمة المحتوى على الرقم بعد التوحيد، فإعادة المحاولة بصيغة رقم مختلفة تُعيد الجواب بدل أن تتعارض.
المسارات
| 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-id | msg_… |
webhook-timestamp | 1767225600 |
webhook-signature | v1,<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 |
انخفض الرصيد عن الحد الذي حددته، أو عن إنفاق الأسبوع الماضي. يتضمن الإشعار التاريخ المتوقع لنفاد الرصيد، ويُرسل مرة واحدة يومياً على الأكثر. |