أرسل قوالب واتساب من نظامك أنت.
واجهة 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 3.1 على رابط ثابت. وجّه أي مولّد إليه لتحصل على عميل مكتوب بلغتك بدل عميل يدوي يتخلّف عن الواجهة مع الوقت.
https://router.atyaf.co/docs/openapi.json
يُعاد توليد المستند من المسارات الحيّة مع كل إصدار، فما هو موجود موصوف وما هو موصوف موجود. المصادقة Bearer على كل عملية، ولا يقبل أي مسار معرّف حساب.
قالب معتمد واحد إلى مستلم واحد. الجواب 202 مع الرسالة في الطابور؛ ويصلك التسليم عبر الويب هوك أو بقراءة الرسالة.
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
$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-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` فلا يتغيّر عبر السلّم — وهو ما تعتمد عليه في منع التكرار، لا الطابع الزمني.
النقطة التي تفشل 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 |
الرصيد دون حدّ الشحن التلقائي الذي حدّدته، أو دون ما أنفقته الأسبوع الماضي، مع اليوم الذي يصل فيه إلى الصفر بالوتيرة الحالية. مرة واحدة يومياً على الأكثر. |