تتيح منصة Scale للمطورين لبرنامجك ربط أرقام واتساب الخاصة بعملائك عبر خطوات الربط الرسمية من Meta، ثم إرسال رسائل واتساب واستقبالها من خلال API واحد من نوع REST ومفتاح API واحد — لرموز التحقق (OTP)، وإشعارات الطلبات والمواعيد، والتنبيهات، وأي رسائل يحتاجها منتجك.
- عنوان الأساس (Base URL):
https://<your Scale site>/wp-json/scale/v1 - السعر: اشتراك ثابت — 1,200 جنيه مصري (EGP 1,200) شهرياً أو 12,000 جنيه مصري (EGP 12,000) سنوياً، ويشمل رقمَي واتساب للحساب كله؛ وكل رقم إضافي له اشتراكه الخاص بسعر 600 جنيه مصري (EGP 600) شهرياً أو 6,000 جنيه مصري (EGP 6,000) سنوياً (§8). لا تفرض Scale أي رسوم على الرسالة ولا تخصم أي رصيد رسائل. أما رسوم محادثات واتساب نفسها فتتبع اتفاق الفوترة بين كل نشاط تجاري وMeta.
- نقل فقط: لا يرد الذكاء الاصطناعي في Scale على رقم أي عميل من عملاء المطورين، ولا يتصرف نيابة عنه أبداً.
1. البدء — من التسجيل إلى أول رسالة
ثماني خطوات، نحو خمس عشرة دقيقة، إضافة إلى الوقت الذي تستغرقه Meta للتحقق من الرقم.
1.1 أنشئ حساب المطوّر
افتح صفحة تسجيل المطورين: https://<your Scale site>/signup/?plan=developer للباقة الشهرية، أو ?plan=developer_annual للباقة السنوية. أدخل اسمك وبريدك الإلكتروني وكلمة المرور واسم شركتك، ثم افتح رابط التحقق الذي ترسله Scale إلى بريدك. حساب المطوّر منفصل عن حساب Scale AI: فلا مساعد ذكي فيه، ولا صندوق وارد، ولا رصيد رسائل.
1.2 اشترك
يبدأ حسابك في وضع الإعداد، وكل ما في هذا الدليل حتى أول رسالة حقيقية يعمل فيه: مفتاح الـAPI، والـwebhooks (واحد للحساب، وواحد لكل رقم إن أردت)، والعملاء، وربط الرقمين المشمولين، والقوالب، وتجربة التكامل (POST /test/webhook، POST /test/messages). أما ما لا يفعله وضع الإعداد فهو الإرسال: إلى أن تؤكد فوترة Scale أول دفعة، يُجاب عن كل رسالة حقيقية بالرمز 403 SUBSCRIPTION_INACTIVE مع details.mode بقيمة setup، ويُجاب عن شراء رقم إضافي بالرمز 402 PAYMENT_REQUIRED (§8). ادفع من صفحة الدفع بعد التحقق، أو لاحقاً من لوحة المطوّرين (الاشتراك والفواتير)؛ وبمجرد تأكيد الدفع يُفتح الإرسال فوراً — ولا تحتاج أبداً إلى التواصل مع Scale.
1.3 أنشئ مفتاح الـAPI
في لوحة المطوّرين افتح مفتاح الـAPI واضغط اعمل مفتاح API. يظهر المفتاح (sk_live_…) مرة واحدة: احفظه فوراً في مخزن الأسرار على خادمك (§10). ثم تحقق منه:
curl -s https://scale.example/wp-json/scale/v1/account \
-H "Authorization: Bearer $SCALE_KEY"
1.4 اضبط عنوان الـwebhook
في لوحة المطوّرين افتح الـWebhooks، وأدخل عنوان https:// الخاص بخادمك، واضغط اعمل العنوان. يظهر سر التوقيع (whsec_…) مرة واحدة. اضغط اتحقق من العنوان: يجب أن يجيب خادمك عن الحدث الموقّع webhook.verification بـ2xx يحتوي جسمه على قيمة data.challenge. ثم أرسل حدث تجربة معلَّماً — ابعت تجربة دلوقتي في اللوحة، أو POST /test/webhook بمفتاحك (§6) — وتأكد أن خادمك يتحقق من توقيعه (§7). ويمكن أيضاً أن يكون لكل رقم عنوانه وسره الخاصان (§7).
1.5 أنشئ عميلاً
العميل هو نشاط تجاري واحد تخدمه. احتفظ بمعرّفك الخاص له في external_ref.
curl -s -X POST https://scale.example/wp-json/scale/v1/customers \
-H "Authorization: Bearer $SCALE_KEY" -H "Content-Type: application/json" \
-d '{"name":"Cairo Clinic","external_ref":"crm-1"}'
يحمل الرد معرّف العميل customer_id (ws_…). احفظه بجوار سجلك.
1.6 اربط رقم واتساب الخاص بالعميل
أنشئ رابط ربط وافتحه بنفسك، أو أرسله إلى النشاط التجاري:
curl -s -X POST https://scale.example/wp-json/scale/v1/customers/ws_XXXX/onboarding-links \
-H "Authorization: Bearer $SCALE_KEY" -H "Content-Type: application/json" \
-d '{"label":"Front desk","state":"crm-1"}'
يفتح الرابط الصفحة المستضافة لدى Scale، والتي تشغّل خطوات الربط الرسمية من Meta (Embedded Signup): يسجّل النشاط التجاري دخوله إلى فيسبوك، ويختار حساب واتساب للأعمال الخاص به أو ينشئه، ثم يتحقق من الرقم بالرمز الذي ترسله Meta. وإذا كان للرقم تحقق بخطوتين، تطلب الصفحة رمز الـPIN المكوّن من 6 أرقام. لا ترى Scale كلمة مرور فيسبوك الخاصة بالنشاط أبداً، ولا تتعامل أنت مع رمز Meta الخاص به.
عند اكتمال الربط يستقبل خادمك الحدثين number.connected وonboarding.completed، ويُدرج GET /numbers الرقم مع معرّفه number_id (num_…).
1.7 أرسل رسالة تجريبية
إلى أن يصبح اشتراكك نشطاً، يقبل POST /test/messages هذا الجسم نفسه ويجيب بما كان الإرسال سيفعله (would_send، blockers) دون أن يرسله. لا يجوز للنشاط التجاري أن يبدأ محادثة إلا بقالب معتمد. اعرض قوالب العميل عبر GET /templates، ثم أرسل أحدها من الرقم:
curl -s -X POST https://scale.example/wp-json/scale/v1/messages \
-H "Authorization: Bearer $SCALE_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: welcome-crm-1" \
-d '{"number_id":"num_XXXX","to":"201012345678","type":"template","template":{"name":"hello_world","language":"en_US"}}'
يستقبل خادمك message.sent، ثم message.delivered وmessage.read كلما أبلغ عنهما واتساب.
1.8 استقبل الرد
عندما يرد الشخص، يستقبل خادمك message.received. وخلال الأربع والعشرين ساعة التالية يمكنك الرد بأي نوع من الرسائل، مثل النص، من الرقم نفسه number_id.
2. المفاهيم والمعرّفات
Your Developer account dev_… (one primary API key)
└─ Customer (one per business you serve) ws_… customer_id
└─ WhatsApp number num_… number_id
- حساب المطوّر (
developer_id،dev_…). حسابك في Scale. يضم الاشتراك ومفتاح الـAPI وعنوان الـwebhook. - العميل (
customer_id،ws_…). عميل واحد لكل نشاط تجاري تخدمه. يمكنك حفظ معرّفك الخاص له فيexternal_ref. لا يسجّل عملاؤك النهائيون الدخول إلى Scale أبداً. - الرقم (
number_id،num_…). رقم واتساب واحد يخص عميلاً.- يمكن أن يملك العميل عدة أرقام، كلها على حساب واتساب للأعمال واحد (WABA).
- يحتفظ الرقم بمعرّفه
number_idما دام مربوطاً، بما في ذلك عند إعادة الربط.
- معرّفات ثابتة. المعرّفات
developer_idوcustomer_idوnumber_idهي معرّفات Scale الخاصة. لا تتغير أبداً ولا يُعاد استخدامها لشيء آخر؛ احفظها في نظامك. - معرّفات Meta بيانات، لا عناوين.
- يُعاد
phone_number_idوwaba_idلتطابقهما مع لوحات Meta. - لا يقبل الـAPI أياً منهما بدلاً من
number_idأوcustomer_id.
- يُعاد
- العزل. كل عميل ورقم ورسالة وقالب ورابط يتبع حساب مطوّر واحداً فقط. والمعرّف الذي يخص حساباً آخر يُجاب عليه بالرمز
404، تماماً كمعرّف غير موجود. - نقل فقط. ما ترسله يُرسل؛ وما يصل يُخزَّن ويُسلَّم إلى الـwebhook الخاص بك. لا يعمل على رقم عميل المطوّر أي رد آلي بالذكاء الاصطناعي، ولا مساعد، ولا متابعة، ولا أتمتة.
3. المصادقة ومفتاح الـAPI
Authorization: Bearer sk_live_…
- مفتاح نشط واحد لكل حساب مطوّر. يصل إلى جميع عملائك وأرقامهم، ولا شيء غير ذلك.
- أين تُدار المفاتيح. تُصدر المفاتيح وتُدوَّر وتُلغى من لوحة المطوّرين فقط. لا يستطيع الـAPI إنشاء مفاتيح.
- التدوير. إصدار مفتاح جديد يوقف المفتاح السابق فوراً.
- الإلغاء. زر ألغِ المفتاح في اللوحة يوقف المفتاح فوراً؛ ويُرفض كل طلب به إلى أن تنشئ مفتاحاً جديداً.
- يظهر مرة واحدة. يظهر المفتاح مرة واحدة عند إصداره. تخزّن Scale بصمته SHA-256 فقط، ولا تعرض بعد ذلك إلا بادئته.
- في الترويسة فقط. أرسل المفتاح في ترويسة
Authorization. يُرفض المفتاح المرسل في الرابط (?api_key=…)، وعليك إلغاء أي مفتاح أُرسل بهذه الطريقة. - مفاتيح مساحات العمل مختلفة. مفتاح مساحة عمل Scale AI (الـPublic API لمساحة عمل واحدة) يعمل تماماً كما كان. لا يمكنه استدعاء مسارات المطورين أدناه (
403 PRODUCT_NOT_ENTITLED)، ولا يمكن لمفتاح المطوّر استدعاء المسارين الخاصين بمساحات العمل/whatsapp/statusو/whatsapp/onboarding/session.
4. الطلبات والردود والتقسيم إلى صفحات
الطلبات. أرسل JSON مع الترويسة Content-Type: application/json. تُرسل المعرّفات في المسار أو في جسم الطلب كما هو موثّق؛ والحقول غير المعروفة تُتجاهل.
النجاح:
{ "success": true, "...": "..." }
الفشل:
{
"success": false,
"error": { "code": "NUMBER_NOT_FOUND", "message": "Number not found.", "details": {} },
"request_id": "req_…"
}
ترويسات في كل رد:
X-Scale-Request-Id— اذكره للدعم الفني.Cache-Control: no-store.X-RateLimit-LimitوX-RateLimit-RemainingوX-RateLimit-Reset(وقت UNIX).Retry-After(بالثواني) مع الرد429.
الطوابع الزمنية بصيغة ISO-8601 بتوقيت UTC (2026-11-10T12:00:00Z).
ما ليس لك يُعامل كغير موجود. المعرّف الذي لا يخصك يُجاب عليه بالرمز 404، تماماً كمعرّف غير موجود.
التقسيم إلى صفحات. القوائم التي يمكن أن تكبر تُقسَّم إلى صفحات بمؤشّر:
GET /customers— الأقدم أولاً؛limitمن 1 إلى 100 (الافتراضي 50)؛ مرّرnext_cursorمن الرد فيafterللحصول على الصفحة التالية.GET /messages— الأحدث أولاً؛limitمن 1 إلى 100 (الافتراضي 50)؛ وnext_cursorبالطريقة نفسها.- تكون قيمة
next_cursorهيnullفي الصفحة الأخيرة. - يعيد
GET /numbersوGET /templatesوروابط ربط العميل كل شيء دفعة واحدة؛ ويأخذGET /usageنطاقاً زمنياً.
عدم التكرار (Idempotency). يقبل POST /messages الترويسة Idempotency-Key حتى لا تؤدي إعادة المحاولة إلى إرسال الرسالة مرتين أبداً (§6).
5. حدود المعدّل
| الحد | الافتراضي | ينطبق على |
|---|---|---|
| لكل مفتاح | 300 طلب / 60 ثانية | كل طلب |
| لكل حساب مطوّر | 600 إرسال / 60 ثانية | POST /messages |
| لكل عميل | 60 إرسال / 60 ثانية | POST /messages لهذا العميل |
| لكل عميل، تغييرات الأرقام | 60 / 10 دقائق | إنشاء روابط الربط وفصل الأرقام |
| لكل حساب مطوّر، تغييرات الحساب | 60 / 10 دقائق | إنشاء العملاء (POST /customers، ومن اللوحة)، ومن اللوحة: إصدار المفتاح أو إلغاؤه، وحفظ الـwebhook أو تدوير سره أو حذفه |
| لكل حساب مطوّر، الأحداث التجريبية | 10 / 10 دقائق | زر "ابعت حدث تجريبي" في اللوحة |
عند تجاوز أي حد تحصل على 429 RATE_LIMITED؛ أعد المحاولة بعد Retry-After. وقد يبطّئ واتساب نفسه رقماً ما، فتجيب Scale حينها بالرمز 429 مع details.reason = "sender_paced".
6. نقاط النهاية (Endpoints)
GET /account
حسابك واشتراكه ومفتاحه.
{
"success": true,
"developer_id": "dev_…",
"name": "Acme Integrations",
"subscription": {
"status": "active",
"plan": "developer",
"period_end": "2026-12-10T12:00:00Z",
"grace_ends_at": "2026-12-13T12:00:00Z",
"can_send": true,
"first_payment_confirmed": true
},
"migration": null,
"customers": { "used": 2, "limit": null },
"numbers": { "included": 2, "additional": 1, "capacity": 3, "connected": 2, "reserved": 1, "available": 0, "max": null, "over_max": false, "uncovered": 0 },
"entitlement": {
"mode": "active",
"can": { "configure": true, "test": true, "send": true },
"checks": { "account_exists": true, "email_verified": true, "api_key_valid": true, "payment_approved": true, "payment_awaiting_review": false, "subscription_active": true },
"subscription_status": "active",
"next_step": null
},
"api_key": { "prefix": "sk_live_AbC123", "created_at": "…", "last_used_at": "…" }
}
قيمة customers.limit هي null: لا تضع Scale حداً لعدد عملائك. وإذا اتفق معك مسؤول في Scale على حد ما، يظهر هنا كرقم.
**numbers** — أرقام واتساب التي يمكن لحسابك ربطها، لكل عملائك معاً: included (رقمان، ضمن اشتراكك)، وadditional (اشتراكات الأرقام الإضافية التي تغطي رقماً الآن)، وcapacity (مجموعهما)، وconnected، وreserved (روابط الربط السارية — كل رابط يحجز رقماً)، وavailable (ما يمكنك إضافته الآن)، وmax (حد أقصى وضعته Scale لحسابك، أو null إن لم يوجد)، وover_max، وuncovered (أرقام متصلة لا يغطيها أي اشتراك — لا يمكنها الإرسال). راجع §8.
**قيم subscription.status:**
nonepending_paymentactivepast_due— خلال مهلة الأيام الثلاثة؛ ما زال الإرسال يعمل.expiredcancelledsuspended
**can_send** يوضح ما إذا كان إرسال الرسائل ممكناً الآن.
**first_payment_confirmed** قيمته false إلى أن تُؤكَّد أول دفعة لاشتراكك: يكون حسابك في وضع الإعداد (§8).
**entitlement** يوضح ما يمكن لحسابك فعله الآن، من الحقائق نفسها التي تفحصها بوابات الخادم: mode (setup — لم يُدفع بعد؛ active؛ paused — انتهت الفترة ومهلتها؛ suspended؛ receive_only — حذف حسابك مطلوب)، وcan (configure، test، send)، وكل حقيقة في checks، وnext_step (create_api_key، activate_subscription، wait_payment_review، renew_subscription، contact_scale، move_numbers، أو null). إنه قراءة وليس إذناً: الإرسال نفسه هو ما يُرفض أو يُقبل، والمفتاح الصالح وحده لا يرسل أبداً. ولكل رقم قراءته الخاصة في GET /numbers.
**migration** قيمته null، أو فترة نقل الأرقام (14 يومًا) التي تمر بها أرقامك (§8.1).
العملاء
**GET /customers?limit=50&after=ws_…** — عملاؤك، الأقدم أولاً، مقسّمين إلى صفحات.
- قيمة
limitبين 1 و100. - للحصول على الصفحة التالية، مرّر قيمة
next_cursorالسابقة فيafter.
{
"success": true,
"customers": [
{
"customer_id": "ws_…",
"name": "Cairo Clinic",
"external_ref": "crm-1",
"created_at": "…",
"numbers": [ { "...": "a number, see /numbers" } ]
}
],
"next_cursor": null
}
**POST /customers** مع {"name": "Cairo Clinic", "external_ref": "crm-1"} يعيد 201 {"customer": {…}}.
- طول
nameمن 1 إلى 120 حرفاً. - الحقل
external_refاختياري: من 1 إلى 190 حرفاً من حروف ASCII المرئية، وفريد بين عملائك أنت. - يعمل في وضع الإعداد، قبل أول دفعة.
**GET /customers/{customer_id}** يعيد {"customer": {…}}.
ربط رقم — روابط الربط
يفتح الرابط الصفحة المستضافة لدى Scale، حيث يربط النشاط التجاري رقم واتساب الخاص به عبر خطوات الربط الرسمية من Meta (Embedded Signup). افتحه بنفسك، أو أرسله إلى عميلك.
**POST /customers/{customer_id}/onboarding-links**
حقول الطلب (كلها اختيارية):
| الحقل | المعنى |
|---|---|
label | اسم الرقم، مثل "الاستقبال". |
state | حتى 200 حرف من حروف ASCII المرئية. يُعاد مع الرابط لتستخدمه في الربط بسجلاتك. |
expires_in | عدد الثواني حتى يتعذر فتح الرابط: من 600 إلى 604800. الافتراضي 259200 (72 ساعة). |
غير مدعوم بعد:
- قيمة
intentغير"add". الرابط دائماً يضيف رقماً. لاستبدال رقم، افصله ثم أضف الجديد. - الحقل
return_url.
كلاهما يُجاب بالرمز 422 INVALID_REQUEST بدلاً من تجاهله.
كل رابط ساري يحجز رقماً من أرقام حسابك — طوال كونه منشأً أو مفتوحاً أو قيد الربط أو في انتظار الرمز السري، إلى أن تنتهي صلاحيته. يُرفض الرابط الجديد بالرمز 402 NUMBER_SUBSCRIPTION_REQUIRED إذا كانت أرقامك المتصلة وروابطك السارية قد استوفت حصتك (رقمان ضمن الاشتراك مع أرقامك الإضافية)، وبالرمز 409 NUMBER_LIMIT_REACHED عند الحد الأقصى الذي وضعته Scale لحسابك. والرابط الذي تنتهي صلاحيته أو يُلغى أو يُلغيه صاحبه أو يفشل يُحرّر رقمه فوراً.
الرد: 201. يظهر link.url في هذا الرد فقط؛ ولا تستطيع Scale عرضه مرة أخرى.
{
"success": true,
"link": {
"link_id": "onb_…",
"customer_id": "ws_…",
"status": "created",
"error": null,
"label": "Front desk",
"state": "crm-1",
"created_via": "api",
"created_at": "…",
"link_expires_at": "…",
"opened_at": null,
"completed_at": null,
"number": null,
"url": "https://<your Scale site>/?scale_onboarding=1#t=…"
}
}
كيف يعمل الرابط:
- يمكن فتحه مرة واحدة. وبعد فتحه يكون أمام النشاط التجاري 30 دقيقة لإكمال الربط.
- الحالات:
created،opened،connecting،pin_required،connected،failed،cancelled،expired،revoked. - عند
connectedيكونlink.numberهو الرقم الذي رُبط، بما فيه معرّفهnumber_id. - يعمل في وضع الإعداد، في حدود الرقمين المشمولين؛ والرقم الثالث يحتاج اشتراك رقم إضافي، وهذا يحتاج أول دفعة (§8).
مسارات مرتبطة:
GET /customers/{customer_id}/onboarding-linksيعيد{"links": […]}.GET /onboarding-links/{link_id}يعيد{"link": {…}}بدون الرابط نفسه.POST /onboarding-links/{link_id}/revokeيعيد{"link": {…, "status": "revoked"}}. لا يمكن إلغاء رابط انتهى (409 ONBOARDING_LINK_FINISHED).
الأرقام
**GET /numbers?customer_id=ws_…** — أرقامك المربوطة؛ وcustomer_id اختياري.
{
"success": true,
"numbers": [
{
"number_id": "num_…",
"customer_id": "ws_…",
"display_phone_number": "+201001234567",
"label": "Front desk",
"phone_number_id": "1234567890",
"waba_id": "9876543210",
"verified_name": "Cairo Clinic",
"status": "connected",
"role": "primary",
"connected_at": "…",
"entitlement": {
"can_send": false,
"checks": { "number_configured": true, "number_connected": true, "number_eligible": true, "api_access": true, "templates_approved": 3 },
"coverage": "included",
"blockers": [ { "code": "SUBSCRIPTION_INACTIVE", "mode": "setup" } ],
"webhook": "number"
}
}
]
}
**GET /numbers/{number_id}** يعيد {"number": {…}}.
**entitlement هي قراءة هذا الرقم وحده: هل يستطيع هو** الإرسال الآن (can_send)، وكل حقيقة (checks)، وموقعه من حصتك (coverage: included أو covered أو uncovered)، وما يمنعه (blockers، كلٌّ بالرمز الذي سيجيب به الإرسال)، وإلى أين تذهب أحداثه (webhook: number — عنوانه الخاص، أو account، أو none). حصة رقم أو ربطه لا تنوب أبداً عن رقم آخر.
**POST /numbers/{number_id}/disconnect** يوقف توجيه Scale للرقم والإرسال منه. لا يُحذف الرقم لدى Meta، ويُحتفظ بسجل المحادثات. ويعيد {"disconnected": {"number_id": "num_…", "customer_id": "ws_…"}}.
الأرقام الإضافية — GET /number-subscriptions
يشمل اشتراكك رقمَي واتساب للحساب كله. ولكل رقم بعدهما اشتراكه الخاص (600 جنيه مصري (EGP 600) شهرياً أو 6,000 جنيه مصري (EGP 6,000) سنوياً)، يُشترى من لوحة المطوّرين (الاشتراك والفواتير) ويُدفع في صفحة الدفع الخاصة بـScale؛ ولا يفعّله إلا الدفع المؤكَّد. هذا المسار للقراءة فقط.
{
"success": true,
"numbers": { "included": 2, "additional": 1, "capacity": 3, "connected": 3, "reserved": 0, "available": 0, "max": null, "over_max": false, "uncovered": 0 },
"prices": [
{ "key": "developer_number", "name": "Additional WhatsApp number", "price": 600, "currency": "EGP", "interval": "month" },
{ "key": "developer_number_annual", "name": "Additional WhatsApp number (Annual)", "price": 6000, "currency": "EGP", "interval": "year" }
],
"subscriptions": [
{
"subscription_id": "nsl_…",
"status": "active",
"covers_number": true,
"interval": "month",
"price": 600,
"currency": "EGP",
"period_start": "2026-11-01T10:00:00Z",
"period_end": "2026-12-01T10:00:00Z",
"grace_ends_at": "2026-12-04T10:00:00Z",
"payment": "paid",
"number": { "number_id": "num_…", "display_phone_number": "+201001234567" },
"created_at": "…",
"cancelled_at": null
}
],
"coverage": [
{ "number_id": "num_…", "role": "included", "subscription_id": null },
{ "number_id": "num_…", "role": "included", "subscription_id": null },
{ "number_id": "num_…", "role": "covered", "subscription_id": "nsl_…" }
]
}
قيمة status هي pending_payment أو active أو cancelled (لن يتجدد؛ ويظل يغطي رقمه حتى نهاية فترته) أو past_due (مهلة الأيام الثلاثة؛ وما زال يغطي) أو expired (لم يعد يغطي) أو void (أُلغي الطلب). وقيمة coverage[].role هي included (أحد رقميك الأولين) أو covered أو uncovered (لا يمكنه الإرسال إلى أن يغطيه اشتراك). تُحتسب أرقامك بترتيب أول ربط لها؛ وفصل أحدها يُحرّر مكانه. وطالما اشتراكك الأساسي موقوف أو انتهى (بعد المهلة)، تتوقف كل أرقامك — المشمولة والمغطاة — ولا يمكن شراء رقم إضافي أو تجديده حتى يُجدَّد الاشتراك الأساسي؛ أما الدفع الذي تم بالفعل فيُطبَّق. وقيمة payment هي unpaid أو awaiting_confirmation أو paid أو complimentary (فترة منحتها Scale دون مقابل — بلا فاتورة) أو refunded (اُسترد مبلغ الدفع ولم يتبقَّ شيء مدفوع). الاسترداد يسترجع بالضبط الفترة التي اشتراها ذلك الدفع: يظل الرقم مربوطًا ويستقبل الرسائل، ويعود للإرسال فور أن يغطيه دفع جديد. وترسل Scale بريدًا لصاحب الحساب قبل انتهاء فترة الرقم الإضافي (قبلها بـ7 أيام ويومين، وعند انتهائها، وعند توقف الإرسال منه).
القوالب
قوالب رسائل العميل كما اعتمدها واتساب على حسابه (WABA). وقوالب الرقم هي قوالب عميله. تُنشأ القوالب وتُعتمد في WhatsApp Manager لدى Meta؛ وScale تقرؤها.
**GET /templates?number_id=num_…** (أو customer_id=ws_…)
- اختياري:
status=approved، وrefresh=1لإعادة قراءتها من Meta. - تقرؤها Scale من Meta عند أول طلب، وعندما تمرّر
refresh=1، وعندما يذكر إرسالٌ قالباً لم تره معتمداً بعد. وتعيد القراءة بحد أقصى مرة واحدة في الدقيقة لكل عميل.
{
"success": true,
"customer_id": "ws_…",
"templates": [
{
"name": "appointment_reminder",
"language": "ar",
"category": "UTILITY",
"status": "approved",
"quality": "GREEN",
"rejection_reason": null,
"components": [
{ "type": "BODY", "text": "مرحباً {{1}}، موعدك {{2}}." }
],
"parameters": { "body": 2, "header": 0 },
"synced_at": "…"
}
],
"sync": { "attempted": false, "ok": null, "error": null, "throttled": false }
}
**GET /templates/{name}?number_id=…&language=ar** يعيد {"customer_id": …, "templates": [ … ]} (أو 404 TEMPLATE_NOT_FOUND إن لم يوجد).
الإرسال — POST /messages
الجسم رسالة إضافة إلى **number_id، وهو الرقم الذي تُرسل منه** (مطلوب). الحقل customer_id اختياري؛ وإن أرسلته فيجب أن يكون عميل هذا الرقم.
ترويسة اختيارية: Idempotency-Key، من 1 إلى 150 حرفاً من حروف ASCII المرئية.
- فريدة على مستوى حسابك كله.
- إعادة المحاولة بالمفتاح نفسه تعيد الرد الأول (
"replayed": true) ولا ترسل مرتين أبداً. - المفتاح نفسه مع جسم مختلف أو رقم مختلف يُجاب بالرمز
409 IDEMPOTENCY_CONFLICT.
أنواع الرسائل:
type | الحقول |
|---|---|
text | text.body (≤ 4000) |
image | image.link (رابط https:// عام)، image.caption? (≤ 1000) |
video | video.link، video.caption? |
audio | audio.link |
document | document.link، document.filename?، document.caption? |
template | template.name، template.language (مثل ar أو en_US)، template.components? (بصيغة Meta، 20 بحد أقصى) |
جسم لكل نوع:
{ "number_id": "num_XXXX", "to": "201012345678", "type": "text", "text": { "body": "Your order 1001 has shipped." } }
{ "number_id": "num_XXXX", "to": "201012345678", "type": "image", "image": { "link": "https://cdn.example.com/receipt-1001.png", "caption": "Your receipt" } }
{ "number_id": "num_XXXX", "to": "201012345678", "type": "video", "video": { "link": "https://cdn.example.com/how-to.mp4", "caption": "How to set up your device" } }
{ "number_id": "num_XXXX", "to": "201012345678", "type": "audio", "audio": { "link": "https://cdn.example.com/voice-note.ogg" } }
{ "number_id": "num_XXXX", "to": "201012345678", "type": "document", "document": { "link": "https://cdn.example.com/invoice-1001.pdf", "filename": "invoice-1001.pdf", "caption": "Invoice 1001" } }
{ "number_id": "num_XXXX", "to": "201012345678", "type": "template", "template": { "name": "order_update", "language": "en_US", "components": [ { "type": "body", "parameters": [ { "type": "text", "text": "Mona" }, { "type": "text", "text": "1001" } ] } ] } }
قواعد تنطبق على كل إرسال:
toرقم دولي كامل، مثل201012345678.- يسمح واتساب بالرسائل الحرة (كل الأنواع عدا
template) فقط خلال 24 ساعة من آخر رسالة أرسلها العميل إلى هذا الرقم. وخارج هذه النافذة أرسل قالباً معتمداً. - يجب أن يكون القالب موجوداً ومعتمداً للعميل، وأن يحصل الجسم على متغيّر واحد بالضبط لكل متغير في القالب. وإلا يكون الرد
422 TEMPLATE_NOT_FOUNDأوTEMPLATE_NOT_APPROVEDأوTEMPLATE_PARAMETER_MISMATCH، ولا يُرسل شيء. - يجب أن تكون روابط الوسائط روابط
https://عامة يستطيع واتساب تنزيلها.
الرد: 200. تعني status: "accepted" أن واتساب قبل الرسالة. يليها التسليم؛ اقرأه عبر GET /messages/{message_id} أو انتظر الـwebhook.
{
"success": true,
"message": {
"id": "msg_…",
"wamid": "wamid.…",
"status": "accepted",
"type": "text",
"to": "201012345678",
"created_at": "…",
"number_id": "num_…",
"customer_id": "ws_…",
"phone_number_id": "1234567890"
},
"replayed": false
}
عندما يرفض واتساب تحصل على 502 META_API_ERROR. لم يُرسل شيء.
**حقول details:**
message_id.outcome: "refused".meta_code— رمز الخطأ لدى Meta.meta_subcode.meta_message— جملة Meta نفسها، 300 حرف بحد أقصى.reason— قراءة Scale:window_expiredأوrecipientأوtemplateأوrate_limitedأوconnectionأوaccountأوmediaأوnetworkأوunsupportedأوunknown.number_id.charged: false.
عندما لا يؤكد واتساب (انتهاء المهلة أو خطأ 5xx) تكون قيمة outcome هي "unknown". قد تكون الرسالة سُلّمت وقد لا تكون. لا تعيد Scale إرسالها أبداً، وإعادة المحاولة بنفس Idempotency-Key تعيد الرد نفسه.
قراءة الرسائل
**GET /messages/{message_id}** — رسالة واحدة أرسلتها، من أي عميل من عملائك.
{
"success": true,
"message": {
"id": "msg_…",
"wamid": "wamid.…",
"type": "text",
"to": "201012345678",
"status": "delivered",
"failure": null,
"created_at": "…",
"number_id": "num_…",
"customer_id": "ws_…",
"phone_number_id": "1234567890"
}
}
**قيم status:** pending، sent، delivered، read، failed، unknown.
- عند
failedتكون قيمةfailure.stageإماsend(رفضها واتساب) أوdelivery(قبلها واتساب ثم تعذّر تسليمها).
**GET /messages?number_id=…&customer_id=…&limit=50&after=msg_…** — رسائلك المرسلة، الأحدث أولاً.
- المرشّحات اختيارية؛ وقيمة
limitبين 1 و100. - الرد هو
{"messages": […], "next_cursor": "msg_…" | null}.
الرسائل التي تستقبلها أرقام عملائك تُسلَّم إلى الـwebhook الخاص بك (message.received)؛ ولا تُدرج هنا.
الاستخدام — GET /usage
أعداد فقط. ليس لحساب المطوّر رصيد رسائل ولا رسوم على الرسالة، لذا لا يوجد حد ولا رقم متبقٍّ.
الاستعلام: from وto تاريخان بتوقيت UTC بصيغة YYYY-MM-DD، والفرق بينهما 92 يوماً بحد أقصى. الافتراضي آخر 30 يوماً. مرشّحات اختيارية: customer_id، number_id.
- يحتوي
daysعلى كل يوم بتوقيت UTC في النطاق بالترتيب، بما فيها الأيام الخالية (كلها أصفار). ومجموع أرقامه يساويtotals. sentوfailedوunknownهي رسائلك المرسلة حسب حالتها؛ وreceivedهو كل رسالة استقبلتها أرقام عملائك.
{
"success": true,
"period": { "from": "2026-10-12", "to": "2026-11-10", "timezone": "UTC" },
"totals": { "sent": 120, "failed": 2, "unknown": 0, "pending": 0, "received": 340 },
"days": [
{ "day": "2026-10-12", "sent": 4, "failed": 0, "unknown": 0, "pending": 0, "received": 11 },
{ "day": "2026-10-13", "sent": 0, "failed": 0, "unknown": 0, "pending": 0, "received": 0 }
],
"numbers": [
{
"number_id": "num_…",
"customer_id": "ws_…",
"phone_number_id": "1234567890",
"sent": 120,
"failed": 2,
"unknown": 0,
"pending": 0,
"received": 340
}
]
}
تجربة التكامل — POST /test/webhook، POST /test/messages
يعمل كلاهما في وضع الإعداد وبعده، بمفتاح المطوّر. لا يصل شيء إلى واتساب: لا تُرسل رسالة، ولا تُنشأ جهة اتصال، ولا يُحتسب أي شيء ولا يدخل طابوراً. ليس لواتساب بيئة تجربة للأرقام المربوطة بـScale (رقم التجربة الذي توفره Meta موجود فقط في تطبيق Meta الخاص بالمطوّر)، لذلك يعتمد هذان المساران على المحاكاة.
**POST /test/webhook** مع {"number_id": "num_…", "event": "message.received"} (كلاهما اختياري) يسلّم حدث تجربة موقّعاً الآن إلى العنوان الذي تذهب إليه أحداث ذلك الرقم — عنوانه الخاص أو عنوان حسابك — ويجيب بما قاله خادمك.
- قيمة
eventهيping(الافتراضية) أوmessage.receivedأوmessage.deliveredأوmessage.failed؛ ويصل الحدث باسمtest.webhookأوtest.message.receivedأوtest.message.deliveredأوtest.message.failed. - البيانات وهمية (المرسل هو
15550100001)، ويحمل الغلاف"livemode": falseو"test": true، ويحمل الطلب الترويسةX-Scale-Test: 1. - يُوقَّع تماماً مثل التسليم الحقيقي، فما يُختبر هو تحقّقك من التوقيع.
{
"success": true,
"test": true,
"livemode": false,
"destination": { "kind": "number", "number_id": "num_…", "host": "hooks.example.com" },
"result": { "delivered": true, "event_id": "evt_test_…", "event": "test.message.received", "http_status": 200, "duration_ms": 184, "error": null, "response_excerpt": "ok" },
"sent_to_whatsapp": false,
"request_id": "req_…"
}
**POST /test/messages** يأخذ جسم POST /messages ويُجري الفحوصات نفسها — الرقم، والجسم، والقالب (مقابل القوالب التي زامنتها Scale؛ ويحدّثها GET /templates?refresh=1)، والاشتراك، وحصة الرقم، ونافذة الـ24 ساعة — ثم يتوقف. يوضح would_send ما إذا كان الإرسال الحقيقي سيتم، وتسرد blockers كل سبب يمنعه، كلٌّ برمز الخطأ الذي يجيب به الإرسال الحقيقي.
{
"success": true,
"test": true,
"livemode": false,
"simulated": true,
"sent_to_whatsapp": false,
"would_send": false,
"blockers": [ { "code": "SUBSCRIPTION_INACTIVE", "message": "Your account is in setup mode: …", "details": { "status": "pending_payment", "mode": "setup" } } ],
"number": { "number_id": "num_…", "customer_id": "ws_…", "phone_number_id": "1234567890" },
"message": { "type": "text", "template": null },
"idempotency_key": null,
"request_id": "req_…"
}
يمكن للحساب إجراء 30 تسليماً تجريبياً و60 فحصاً دون إرسال كل 10 دقائق. وتُجريها لك صفحة تجربة التكامل في اللوحة، وتعرض آخر طلبات الـAPI مع request_id لكل منها.
7. الـWebhooks — أحداث تُرسل إلى خادمك
يستقبل عنوان الحساب أحداث كل رقم ليس له عنوان خاص به؛ ويمكن أن يكون لرقمٍ عنوانه الخاص (راجع «عنوان لكل رقم» أدناه).
- تضبطه، وترى عمليات التسليم إليه، من لوحة المطوّرين (الـWebhooks).
- لا يستطيع مفتاح الـAPI تغييره، فلا يمكن لمفتاح مسرّب إعادة توجيه أحداثك.
- يظهر سر العنوان (
whsec_…) مرة واحدة، عند إنشاء العنوان أو تدوير السر.
الأحداث
| الحدث | متى | data |
|---|---|---|
message.received | كتب شخص إلى أحد أرقامك: نص أو صورة أو صوت أو رسالة صوتية أو فيديو أو مستند أو موقع أو رد على زر أو قائمة أو طلب. لا تُسلَّم الملصقات وبطاقات جهات الاتصال والتفاعلات. | message_id (معرّف واتساب)، from، type، text أو media، contact.name، timestamp |
message.sent | قبل واتساب رسالة أُرسلت من أحد أرقامك. | message_id، api_message_id (msg_…)، to، type، source، status: "sent"، text / template، timestamp |
message.delivered، message.read، message.failed | أبلغ واتساب عن تقدّم. | message_id، api_message_id، to، status، error (عند الفشل فقط)، timestamp |
number.connected | رُبط رقم (عبر رابط ربط، أو إعادة ربط). | number (كما في GET /numbers/{number_id})، timestamp |
number.disconnected | توقف توجيه رقم. | number_id، phone_number_id، reason، timestamp |
onboarding.completed، onboarding.failed | انتهى رابط ربط. | link_id، ثم number_id (عند الاكتمال) أو error (عند الفشل)، timestamp |
webhook.test | ضغطت "ابعت حدث تجريبي". | message، timestamp |
في number.disconnected تكون قيمة reason إحدى:
developer— أنت، عبر الـAPI أو اللوحة؛admin — Scale؛released— أي سبب آخر، مثل ربط الرقم في مكان آخر.
يمكنك قصر العنوان على بعض هذه الأحداث. ويُرسل webhook.test دائماً.
الغلاف
{
"event": "message.received",
"id": "evt_…",
"api_version": "2026-10",
"created_at": "2026-11-10T12:00:00Z",
"developer_id": "dev_…",
"customer_id": "ws_…",
"number_id": "num_…",
"phone_number_id": "1234567890",
"data": { "…": "…" }
}
تكون قيمة number_id وphone_number_id هي null في حدث لا يخص رقماً بعينه (onboarding.failed، webhook.test).
التسليم
الطلب: POST بجسم JSON.
الترويسات:
| الترويسة | القيمة |
|---|---|
X-Scale-Event | نوع الحدث. |
X-Scale-Event-Id | evt_… |
X-Scale-Timestamp | ثواني UNIX لهذه المحاولة. |
X-Scale-Signature | sha256= + توقيع HMAC-SHA256 للنص "<timestamp>.<raw body>" بسرّك. |
X-Scale-Delivery-Attempt | رقم المحاولة. |
X-Scale-Signature-Previous | خلال 24 ساعة بعد تدوير السر فقط: التوقيع نفسه، مصنوعاً بالسر السابق. |
إعادة المحاولات:
- أجب بـ
2xxخلال 5 ثوانٍ. - أي رد آخر يُعاد إرساله: بعد 1 و2 و5 و15 و30 دقيقة، ثم بعد ساعة وساعتين، حتى 8 محاولات.
- الجسم و
idمتطابقان في كل محاولة.
مرة واحدة على الأقل — تعامل مع التكرار. قد يصل الحدث نفسه أكثر من مرة (مثل إعادة المحاولة بعد انتهاء المهلة). أزل التكرار باستخدام id (X-Scale-Event-Id): خزّن كل id عالجته، وأجب بـ200 دون معالجته مرة أخرى.
الترتيب. الأحداث المتعلقة بالرسالة نفسها تصل بالترتيب. أما أحداث الرسائل المختلفة فقد لا تصل بالترتيب.
قواعد لعنوانك:
- يجب أن يكون
https://. وتُتبع إعادات التوجيه على المضيف نفسه فقط وإلىhttps://فقط. - تظهر أول 200 حرف من رد عنوانك في لوحتك (بعد إزالة الوسوم). لا تُعِد الأسرار في ردك.
- أجب بسرعة: سجّل الحدث، وأجب بـ
200، ثم نفّذ العمل البطيء في طابورك الخاص.
تستمر الأحداث في الوصول أثناء توقف الاشتراك (الإرسال هو ما يتوقف — §8). ولا تتوقف إلا إذا أُغلق حساب المطوّر نفسه.
التحقق من التسليم
احسب التوقيع على الجسم الخام كما وصل تماماً، قبل تحليله. اقبل أياً من ترويستي التوقيع: أثناء التدوير، يطابق السرُّ الذي تملكه إحداهما. وارفض الطابع الزمني القديم لمنع إعادة الإرسال الخبيثة.
<?php
// scale-webhook-verify
function scale_webhook_is_valid( string $secret, string $raw_body, array $headers, int $tolerance = 300, ?int $now = null ): bool {
$headers = array_change_key_case( $headers, CASE_LOWER );
$ts = (string) ( $headers['x-scale-timestamp'] ?? '' );
$now = $now ?? time();
if ( ! ctype_digit( $ts ) || abs( $now - (int) $ts ) > $tolerance ) {
return false; // missing, or too old: refuse a replay
}
$expected = 'sha256=' . hash_hmac( 'sha256', $ts . '.' . $raw_body, $secret );
foreach ( [ 'x-scale-signature', 'x-scale-signature-previous' ] as $name ) {
if ( isset( $headers[ $name ] ) && hash_equals( $expected, (string) $headers[ $name ] ) ) {
return true;
}
}
return false;
}
// In your endpoint:
// $raw = file_get_contents( 'php://input' );
// if ( ! scale_webhook_is_valid( getenv( 'SCALE_WEBHOOK_SECRET' ), $raw, getallheaders() ) ) { http_response_code( 400 ); exit; }
// $event = json_decode( $raw, true ); // then deduplicate on $event['id'] and answer 200 quickly
// scale-webhook-verify
const crypto = require('crypto');
function scaleWebhookIsValid(secret, rawBody, headers, toleranceSeconds = 300, now = Math.floor(Date.now() / 1000)) {
const h = Object.fromEntries(Object.entries(headers).map(([k, v]) => [k.toLowerCase(), String(v)]));
const ts = h['x-scale-timestamp'] || '';
if (!/^\d+$/.test(ts) || Math.abs(now - Number(ts)) > toleranceSeconds) return false; // too old: refuse a replay
const expected = Buffer.from('sha256=' + crypto.createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex'));
return ['x-scale-signature', 'x-scale-signature-previous'].some((name) => {
const got = Buffer.from(h[name] || '');
return got.length === expected.length && crypto.timingSafeEqual(got, expected);
});
}
module.exports = { scaleWebhookIsValid };
// In Express: app.post('/scale', express.raw({ type: 'application/json' }), (req, res) => {
// if (!scaleWebhookIsValid(process.env.SCALE_WEBHOOK_SECRET, req.body.toString('utf8'), req.headers)) return res.sendStatus(400);
// const event = JSON.parse(req.body); /* deduplicate on event.id */ res.sendStatus(200);
// });
عنوان لكل رقم
تُرسل Meta أحداث أرقامك إلى عنوان استدعاء Scale، لا إلى عنوانك أبداً: لتطبيق Scale عنوان استدعاء واحد، وتتحقق Scale من توقيع Meta X-Hub-Signature-256 على الجسم الخام. تستطيع Meta إرسال أحداث حساب واتساب للأعمال أو رقم هاتف إلى عنوان آخر، لكن Scale لا توجّه ذلك إلى خادمك — فـMeta توقّع بسر تطبيق Scale الذي لا يمكنك التحقق منه، وكانت Scale ستتوقف عن رؤية الرسائل والحالات التي تعتمد عليها عمليات إرسالك وبواباتها. بدلاً من ذلك، تُوصِل Scale أحداث كل رقم إلى العنوان الخاص بذلك الرقم:
- اضبطه من اللوحة (الـWebhooks ← عنوان لكل رقم ← اظبط): عنوانه وأحداثه وسرّه
whsec_…الخاص، ويظهر السر مرة واحدة. - الحدث الواحد يذهب إلى عنوان واحد: عنوان الرقم الخاص، أو — إن لم يكن له عنوان — عنوان حسابك. أحداث رقم آخر لا تصل إليه أبداً، ولا يُوقَّع أبداً بسر آخر.
- لكل عنوان حالته الخاصة: التحقق، وآخر تسليم ورمز HTTP الخاص به، وآخر خطأ، وعدد الأحداث التي تنتظر إعادة المحاولة.
- دوّر سر الرقم، أو أنهِ فترة التداخل فوراً (وقّف السر القديم دلوقتي)، أو احذف العنوان (يُحذف سره؛ وتذهب أحداث الرقم التالية إلى عنوان حسابك).
- تتحقق Scale من أن أحداث الرقم تصل تحت حساب واتساب للأعمال الذي رُبط تحته؛ والرقم الذي يصل تحت حساب آخر لا يُقبل.
- لا يستطيع مفتاح الـAPI قراءة أي عنوان أو تغييره.
أحداث التحقق والتجربة
| الحدث | متى | data |
|---|---|---|
webhook.verification | ضغطت اتحقق من العنوان. أجب بـ2xx وفي الجسم قيمة التحدي. | challenge، message |
test.webhook، test.message.received، test.message.delivered، test.message.failed | تجربة (POST /test/webhook، أو من اللوحة). بيانات وهمية. | مثل الحدث الحقيقي، مع "livemode": false و"test": true في الغلاف |
كلها تحمل X-Scale-Test: 1، وتُوقَّع مثل التسليم الحقيقي، وتُسلَّم فوراً — لا توضع في طابور ولا تُعاد محاولتها. لا تعاملها أبداً كرسالة من عميل.
8. الاشتراك والدفع والإرسال
| متى | ماذا يحدث |
|---|---|
| مسجّل، ولم تُؤكَّد أول دفعة (وضع الإعداد) | كل شيء يعمل ما عدا الإرسال: مفتاح الـAPI، والـwebhooks، والعملاء، وربط الرقمين المشمولين، والقوالب، والتجارب (POST /test/webhook، POST /test/messages). وتُرفض كل رسالة حقيقية — رمز OTP أو رسالة تحقق أو تسويق أو خدمة، قالباً كانت أو نصاً حراً: 403 SUBSCRIPTION_INACTIVE، مع details.mode بقيمة setup. ويُرفض شراء رقم إضافي: 402 PAYMENT_REQUIRED، مع details.subscription_status. |
| مدفوع | يعمل الإرسال من كل رقم متصل ومغطّى — الرقمان المشمولان، وكل رقم إضافي ما دام اشتراكه الخاص سارياً. |
| انتهت الفترة، وخلال 3 أيام تقويمية | كل شيء ما زال يعمل (past_due). جدّد اشتراكك. |
| بعد الأيام الثلاثة | يُرفض كل إرسال من كل رقم لكل عميل: 403 SUBSCRIPTION_INACTIVE، مع details.status وdetails.grace_ends_at وdetails.migration. وتبدأ فترة نقل الأرقام (14 يومًا) (§8.1). |
| أوقفت Scale الحساب | مثل ما بعد الأيام الثلاثة (قيمة details.status هي suspended). وتبدأ فترة نقل الأرقام (14 يومًا) (§8.1). |
| أكّدت حذف حسابك | يصبح الحساب للاستقبال فقط لمدة 14 يومًا تقويميًا قبل تنفيذ الحذف: يُرفض الإرسال وإنشاء العملاء وإنشاء روابط الربط بالرمز 403 ACCOUNT_RECEIVE_ONLY (details.migration)، ولا يعود رابط أُنشئ قبل ذلك قادرًا على ربط رقم. وتظل القراءة وفصل الأرقام تعمل، وتظل الرسائل الواردة تصل إلى الـwebhook. وإلغاء الحذف (من الرابط في بريد Scale) ينهي ذلك فورًا. |
| الرقم الثالث وكل رقم بعده | يحتاج اشتراك رقم إضافي خاصاً به (600 جنيه مصري (EGP 600) شهرياً أو 6,000 جنيه مصري (EGP 6,000) سنوياً). وبدونه يُرفض رابط الربط الجديد: 402 NUMBER_SUBSCRIPTION_REQUIRED. |
| ينتهي اشتراك رقم إضافي (بعد مهلته الخاصة، 3 أيام) | يظل الرقم متصلاً ويستمر في الاستقبال؛ ولا يُرفض إلا الإرسال منه: 403 NUMBER_SUBSCRIPTION_INACTIVE. جدّده من لوحة التحكم (الاشتراك والفواتير). |
| عند الحد الأقصى الذي وضعته Scale لحسابك | تُرفض الروابط والأرقام الجديدة: 409 NUMBER_LIMIT_REACHED. ولا يتأثر أي رقم متصل. |
التجديد يعيد الإرسال فوراً، بالمعرّفات نفسها. ولا حاجة لإعادة الربط.
كيف تُؤكَّد الدفعة. الدفع عبر InstaPay: تعرض Scale عند الدفع رقم InstaPay والمبلغ بالضبط؛ تحوّل المبلغ وترفع إيصال التحويل مع اسم المحوِّل والرقم المرجعي. بعدها تنتظر الدفعة المراجعة — يراجع شخص من Scale التحويل مقابل الإيصال، ولا يُفعَّل أي شيء قبل أن يوافق عليه. الدفعة المقبولة تفعّل ما دُفعت له بالضبط (فترة الاشتراك الأساسي، أو فترة ذلك الرقم الإضافي). والدفعة المرفوضة لا تفعّل شيئًا؛ وتعرض لوحة التحكم (الاشتراك والفواتير ← المدفوعات) كل دفعة: قيد المراجعة أو مقبولة أو مرفوضة أو ملغاة أو مستردة، مع الخطوة التالية. والعودة من صفحة الدفع وحدها لا تثبت شيئاً. وأول دفعة مقبولة تفتح الإرسال فوراً — دون أي طلب إلى Scale. وما دامت دفعة للاشتراك الأساسي قيد المراجعة لا يمكن بدء دفعة ثانية له.
8.1 فترة نقل الأرقام (14 يومًا)
عندما يتوقف الإرسال لحسابك كله — انتهت مهلة الأيام الثلاثة، أو أوقفت Scale الحساب، أو أكّدت حذف الحساب — تدخل أرقامك فترة نقل مدتها 14 يومًا تقويميًا، حتى تنقلها دون أن يضيع ما يصل إلى عملائك:
- لا يُحذف شيء ولا يُفصل شيء. يظل كل رقم مربوطًا بالعميل نفسه، بالمعرّفات نفسها.
- تظل الرسائل الواردة تُستقبل وتُخزَّن وتُرسل إلى الـwebhook، مع إعادة المحاولة المعتادة، حتى الموعد النهائي.
- يُرفض كل إرسال، من كل مسار وفي كل إعادة محاولة.
- تظل القراءة (
/accountو/customersو/numbersو/messagesو/usageو/templates) وPOST /numbers/{number_id}/disconnectتعمل بمفتاح الـAPI نفسه. - لا يعمل أي رد ذكاء اصطناعي أو أتمتة أو تحويل إعلاني على رقم مطوّرين، لا في هذه الفترة ولا في أي وقت (§2).
- يُرجع
GET /accountالحقلmigration، ويحمل كل طلب مرفوض الكائن نفسه فيdetails.migration:
"migration": {
"reason": "subscription",
"started_at": "2026-12-13T12:00:00Z",
"deadline": "2026-12-27T12:00:00Z",
"days_remaining": 9,
"inbound_forwarding": "on",
"deletion_at": null
}
قيمة reason هي subscription (انتهت المهلة) أو suspended أو deletion. وdeletion_at هو موعد تنفيذ الحذف المطلوب — بعد 14 يومًا من تأكيده، ولا يكون أبكر من ذلك أبدًا — أو null. وتصبح قيمة inbound_forwarding هي stopped بعد الموعد النهائي.
الموعد النهائي يُثبَّت عند بدء الفترة: بعد 14 يومًا من أول هذه الأحداث: انتهاء المهلة، وبدء الإيقاف، وتأكيد الحذف. ولا تؤخره إعادة محاولات التسليم، ولا تكرار الطلبات، ولا طلب حذف أثناء فترة جارية.
كيف تنتهي الفترة:
- تجدّد اشتراكك (أو ترفع Scale الإيقاف): يعود الإرسال فورًا وتنتهي الفترة. ولا حاجة لإعادة ربط أي شيء.
- تلغي حذفًا مطلوبًا: يخرج الحساب من وضع الاستقبال فقط فورًا (والإرسال ما زال يحتاج اشتراكًا فعّالًا).
- يمر الموعد النهائي بعد انتهاء الاشتراك أو إيقافه: تظل الرسائل الواردة تُستقبل وتُخزَّن، لكنها لا تُرسل إلى الـwebhook. ولا يُحذف شيء ولا يُفصل شيء، والتجديد يعيد كل شيء.
- يمر الموعد النهائي على حذف مطلوب: يُنفَّذ الحذف — تُزال بيانات عملائك، وربط أرقامهم بـScale، ومحادثاتهم، ومفتاح الـAPI، وعنوان الـwebhook. وتُحفظ سجلات الفوترة كما يقتضي القانون. ولا يحدث ذلك أبدًا قبل الموعد النهائي.
نقل رقم. لا تنقل Scale الأرقام نيابةً عنك. كل رقم يتبع حساب واتساب للأعمال الخاص بعميلك: لنقله، اربطه بمزوّده الجديد وفق تعليمات ذلك المزوّد وMeta، ثم افصله هنا عبر POST /numbers/{number_id}/disconnect (تعرض شاشة الأرقام في لوحة التحكم كل رقم مع عميله ومعرّفاته). واسأل المزوّد الجديد إن كان يجب فصل الرقم عن Scale أولًا.
ما تدفعه. 1,200 جنيه مصري (EGP 1,200) شهرياً أو 12,000 جنيه مصري (EGP 12,000) سنوياً، للحساب كله — كل عميل، ويشمل رقمَي واتساب. كل رقم إضافي: 600 جنيه مصري (EGP 600) شهرياً أو 6,000 جنيه مصري (EGP 6,000) سنوياً، باشتراك خاص به يظهر منفصلاً في فواتيرك — مثلاً 3 أرقام تكلّف 1,800 جنيه مصري (EGP 1,800) شهرياً و5 أرقام 3,000 جنيه مصري (EGP 3,000) شهرياً (18,000 و30,000 جنيه مصري في السنة). لا تفرض Scale أي رسوم على الرسالة ولا تخصم أي رصيد رسائل؛ وGET /usage يكتفي بالعدّ، ولا تضع Scale حداً لعدد الرسائل. أما رسوم محادثات واتساب نفسها فهي رسوم Meta، وتُحصَّل من كل نشاط تجاري وفق ترتيب حسابه في واتساب للأعمال.
9. الأخطاء وحل المشكلات
| الرمز | HTTP | متى |
|---|---|---|
INVALID_API_KEY | 401 | لا مفتاح، أو مفتاح بصيغة خاطئة، أو مفتاح في الرابط. |
API_KEY_REVOKED | 401 | أُلغي، أو استُبدل بمفتاح أحدث. |
PAYMENT_REQUIRED | 402 | شراء رقم إضافي قبل تأكيد أول دفعة لاشتراكك (§8). ويمكن ربط الرقمين المشمولين قبلها. |
NUMBER_SUBSCRIPTION_REQUIRED | 402 | أرقامك وروابطك السارية استوفت حصتك: أضف رقماً إضافياً من لوحة التحكم (الاشتراك والفواتير)، أو انتظر انتهاء صلاحية رابط (§8). |
PRODUCT_NOT_ENTITLED | 403 | مفتاح مساحة عمل على مسار للمطورين، أو مفتاح مطوّر على مسار خاص بمساحات العمل. |
SUBSCRIPTION_INACTIVE | 403 | الإرسال في وضع الإعداد (قبل أول دفعة)، أو بعد انتهاء المهلة، أو أثناء إيقاف Scale للحساب — يوضح details.mode أيها (setup، paused، suspended)، مع details.status وdetails.migration (§8، §8.1). |
ACCOUNT_RECEIVE_ONLY | 403 | حذف حسابك مطلوب: يُرفض الإرسال وإنشاء العملاء وروابط الربط الجديدة حتى يُنفَّذ الحذف أو تلغيه (details.migration، §8.1). وتظل الرسائل الواردة تصل إلى الـwebhook. |
NUMBER_SUBSCRIPTION_INACTIVE | 403 | انتهى اشتراك الرقم الإضافي لهذا الرقم (details.number_id، details.subscription_id). جدّده؛ لم يُفصل أي شيء. |
WHATSAPP_NOT_CONNECTED | 403 | لا يمكن للرقم الإرسال الآن (details.reason). أعد ربطه برابط ربط جديد. |
API_ACCESS_DISABLED | 403 | أوقفت Scale الـAPI لهذا العميل (مساراته وأحداث الـwebhook الخاصة به). تواصل مع Scale. |
INVALID_REQUEST | 422 | حقل مفقود أو بصيغة خاطئة (details.field). |
INVALID_MESSAGE | 400 / 422 | جسم الرسالة غير صالح. |
INVALID_PHONE_NUMBER | 422 | to ليس رقماً دولياً كاملاً. |
INVALID_CUSTOMER | 422 | قيمة غير صالحة لـname أو external_ref الخاصين بالعميل. |
CUSTOMER_NOT_FOUND | 404 | ليس من عملائك. |
NUMBER_NOT_FOUND | 404 | ليس من أرقامك المربوطة. معرّف رقم الهاتف لدى Meta ليس number_id أبداً. |
ONBOARDING_LINK_NOT_FOUND | 404 | ليس من روابطك. |
MESSAGE_NOT_FOUND | 404 | ليس من رسائلك. |
WEBHOOK_NOT_FOUND | 404 | لم يجد الاختبار أو التحقق أي عنوان: لا للرقم ولا لحسابك. |
TEMPLATE_NOT_FOUND | 404 / 422 | القالب غير معروف لهذا العميل. |
CUSTOMER_NUMBER_MISMATCH | 422 | customer_id وnumber_id لا يتطابقان. |
CUSTOMER_REF_IN_USE | 409 | عميل آخر من عملائك يملك هذه القيمة لـexternal_ref. |
CUSTOMER_LIMIT_REACHED | 409 | حد للعملاء وضعه مسؤول في Scale على حسابك (لا يوجد حد افتراضياً). تواصل مع Scale لرفعه. |
NUMBER_LIMIT_REACHED | 409 | الحد الأقصى لأرقام واتساب الذي وضعته Scale لحسابك. تواصل مع Scale لرفعه. |
ONBOARDING_LINK_FINISHED | 409 | انتهى الرابط ولا يمكن إلغاؤه. |
IDEMPOTENCY_CONFLICT | 409 | استُخدم Idempotency-Key لطلب مختلف. |
OUTSIDE_MESSAGING_WINDOW | 422 | رسالة حرة خارج نافذة الـ24 ساعة. أرسل قالباً. |
TEMPLATE_NOT_APPROVED | 422 | القالب غير معتمد من واتساب. |
TEMPLATE_PARAMETER_MISMATCH | 422 | عدد خاطئ من متغيّرات الجسم (details.expected، details.given). |
RATE_LIMITED | 429 | تجاوز حد معدّل (انظر §5). |
META_API_ERROR | 502 | رفض واتساب الرسالة، أو لم يؤكدها (details.outcome). |
INTERNAL_ERROR | 500 | خطأ من جهة Scale. اذكر request_id. |
حل المشكلات
- **كل استدعاء يُجاب بـ
401.** أرسل المفتاح بصيغةAuthorization: Bearer …، ولا ترسله في الرابط أبداً. وإن كنت دوّرت المفتاح فقد توقف القديم فوراً — استخدم الجديد. - **
403 SUBSCRIPTION_INACTIVEمعdetails.modeبقيمةsetup.** لم تُؤكَّد أول دفعة بعد، فلا يُرسل شيء. كل ما عدا ذلك يعمل؛ افحص رسائلك عبرPOST /test/messages. ادفع من لوحة المطوّرين (الاشتراك والفواتير)؛ ويُظهرGET /accountقيمةfirst_payment_confirmed. وقد تكون دفعة InstaPay قمت بها ما زالت قيد المراجعة — تعرض «الاشتراك والفواتير ← المدفوعات» حالتها. - **
403 SUBSCRIPTION_INACTIVEمعdetails.migration.** انتهت المهلة (أو أوقفت Scale الحساب). تظل الرسائل الواردة تصل إلى الـwebhook حتىdetails.migration.deadline؛ جدّد لتعيد الإرسال، أو انقل أرقامك قبل ذلك (§8.1). - **
403 ACCOUNT_RECEIVE_ONLY.** حذف حسابك مطلوب. يُرفض الإرسال وإضافة عملاء أو روابط ربط جديدة حتى يُنفَّذ الحذف (details.migration.deletion_at) أو تلغيه من الرابط في بريد Scale. - **
404 NUMBER_NOT_FOUNDلرقم تراه في Meta.** أرسلتphone_number_idالخاص بـMeta. أرسل معرّف Scalenumber_id (num_…) منGET /numbers. - **
422 OUTSIDE_MESSAGING_WINDOW.** لم يكتب الشخص إلى هذا الرقم خلال آخر 24 ساعة. أرسل قالباً معتمداً. - **
422 TEMPLATE_PARAMETER_MISMATCH.** يحتاج الجسم متغيّراً واحداً لكل متغير{{n}}؛ ويُظهرGET /templatesالقيمةparameters.body. - **
429 RATE_LIMITED.** انتظرRetry-Afterثانية. ومعdetails.reason = "sender_paced"يكون واتساب يبطّئ هذا الرقم: وزّع رسائلك على فترة أطول. - **
502 META_API_ERROR.** اقرأdetails.reasonوdetails.meta_code. تعنيoutcome: "unknown"أن واتساب لم يؤكد: لا تُعِد الإرسال بمفتاح جديد؛ أعد المحاولة بنفسIdempotency-Keyلتقرأ الرد نفسه. - لا يصل أي webhook. يجب أن يكون العنوان
https://، وأن يجيب بـ2xxخلال 5 ثوانٍ، وألا يعيد التوجيه إلى مضيف آخر. تُظهر لوحة المطوّرين (الـWebhooks) كل عملية تسليم ومحاولاتها ورد عنوانك. - التوقيعات لا تتطابق أبداً. احسب البصمة على الجسم الخام كما وصل تماماً، قبل أي تحليل لـJSON؛ واستخدم ترويسة الطابع الزمني كما أُرسلت؛ واضبط ساعة خادمك (تسمح الأمثلة بفارق 300 ثانية).
- **يظهر لرقم
WHATSAPP_NOT_CONNECTED.** يحتاج ربطه إلى تجديد لدى Meta: أنشئ رابط ربط جديداً للعميل.
10. أفضل ممارسات الأمان
- أبقِ المفتاح على خادمك. لا تضعه أبداً في متصفح أو تطبيق جوال أو مستودع عام أو رابط. حمّله من مدير أسرار أو من متغير بيئة.
- دوّر المفتاح عند أي شك. التدوير من اللوحة يوقف المفتاح القديم فوراً؛ والإلغاء يوقف كل وصول إلى أن تنشئ مفتاحاً جديداً.
- تحقّق من كل webhook. افحص التوقيع على الجسم الخام وارفض الطوابع الزمنية القديمة؛ ولا تثق أبداً بطلب غير موقّع يبدو كأنه من Scale.
- احفظ سر الـwebhook كما تحفظ المفتاح. دوّره من اللوحة إن احتمل تسرّبه؛ ولمدة 24 ساعة يوقّع السرّان معاً، فتنتقل دون أن يفوتك أي تسليم.
- سر لكل عنوان. تحقّق من كل تسليم بسر العنوان الذي استقبله — لعنوان الرقم الخاص سره الخاص — ولا تعدّ أبداً حدث تجربة (
X-Scale-Test: 1،"test": true) رسالةً من عميل. - أزل تكرار الأحداث باستخدام
id، واجعل معالجاتك آمنة إذا نُفذت مرتين. - **استخدم
Idempotency-Key** مع كل إرسال قد تعيد محاولته. - افصل بين مستخدميك. مفتاح واحد يصل إلى جميع عملائك. تفصل Scale حسابك عن كل مطوّر آخر، لكن داخل حسابك يجب أن يربط تطبيقك كل مستخدم من مستخدميك بمعرّف
customer_idالخاص به، وألا يسمح أبداً لمستخدم بتسمية عميل أو رقم يخص غيره. - عامل روابط الربط كأسرار. يعمل الرابط مرة واحدة وينتهي؛ أرسله فقط إلى النشاط التجاري المقصود، وألغِ الروابط التي لم تعد تحتاجها.
- أقل قدر من البيانات. أرسل فقط ما تحتاجه الرسالة؛ ولا تضع أبداً كلمات مرور أو أرقام بطاقات كاملة في رسالة.
- رموز التحقق لا تُحفظ. عند إرسال قالب من فئة AUTHENTICATION تسجّل Scale عبارة «تم إرسال رمز التحقق» ولا تحتفظ بالرمز ولا بقيم متغيّرات القالب، ولا يحمل الحدث
message.sentأي رمز. أبقِ الرموز خارج ما تخزّنه Scale كما ترسله:Idempotency-Keyوexternal_refوstateالخاص برابط الربط.
11. أمثلة
cURL
# Your customers and their numbers
curl -s https://scale.example/wp-json/scale/v1/numbers \
-H "Authorization: Bearer $SCALE_KEY"
# A customer, then a link to connect its number
curl -s -X POST https://scale.example/wp-json/scale/v1/customers \
-H "Authorization: Bearer $SCALE_KEY" -H "Content-Type: application/json" \
-d '{"name":"Cairo Clinic","external_ref":"crm-1"}'
curl -s -X POST https://scale.example/wp-json/scale/v1/customers/ws_XXXX/onboarding-links \
-H "Authorization: Bearer $SCALE_KEY" -H "Content-Type: application/json" \
-d '{"label":"Front desk","state":"crm-1"}'
# Send a text from one number (safe to retry with the same Idempotency-Key)
curl -s -X POST https://scale.example/wp-json/scale/v1/messages \
-H "Authorization: Bearer $SCALE_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: order-1001-confirmation" \
-d '{"number_id":"num_XXXX","to":"201012345678","type":"text","text":{"body":"Your order shipped."}}'
# Send an approved template
curl -s -X POST https://scale.example/wp-json/scale/v1/messages \
-H "Authorization: Bearer $SCALE_KEY" -H "Content-Type: application/json" \
-d '{"number_id":"num_XXXX","to":"201012345678","type":"template","template":{"name":"appointment_reminder","language":"ar","components":[{"type":"body","parameters":[{"type":"text","text":"Mona"},{"type":"text","text":"5 pm"}]}]}}'
# Where does it stand?
curl -s https://scale.example/wp-json/scale/v1/messages/msg_XXXX \
-H "Authorization: Bearer $SCALE_KEY"
PHP
<?php
function scale_api( string $method, string $path, ?array $body = null, array $headers = [] ): array {
$ch = curl_init( 'https://scale.example/wp-json/scale/v1' . $path );
$h = array_merge( [ 'Authorization: Bearer ' . getenv( 'SCALE_KEY' ), 'Content-Type: application/json' ], $headers );
curl_setopt_array( $ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $h,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
] );
if ( $body !== null ) {
curl_setopt( $ch, CURLOPT_POSTFIELDS, json_encode( $body ) );
}
$raw = curl_exec( $ch );
$status = (int) curl_getinfo( $ch, CURLINFO_HTTP_CODE );
curl_close( $ch );
$json = json_decode( (string) $raw, true );
if ( $status >= 400 ) {
throw new RuntimeException( ( $json['error']['code'] ?? 'HTTP_' . $status ) . ': ' . ( $json['error']['message'] ?? '' ) . ' (' . ( $json['request_id'] ?? '' ) . ')' );
}
return $json;
}
// Send from a customer's number; the Idempotency-Key makes a retry safe.
$sent = scale_api( 'POST', '/messages', [
'number_id' => 'num_XXXX',
'to' => '201012345678',
'type' => 'text',
'text' => [ 'body' => 'Your order shipped.' ],
], [ 'Idempotency-Key: order-1001-confirmation' ] );
echo $sent['message']['id'], ' from ', $sent['message']['number_id'], PHP_EOL;
// Connect a new customer's number: create the customer, then a link to send them.
$customer = scale_api( 'POST', '/customers', [ 'name' => 'Cairo Clinic', 'external_ref' => 'crm-1' ] );
$link = scale_api( 'POST', '/customers/' . $customer['customer']['customer_id'] . '/onboarding-links', [ 'label' => 'Front desk' ] );
echo $link['link']['url'], PHP_EOL;
Node.js (18+)
const BASE = 'https://scale.example/wp-json/scale/v1';
async function scaleApi(method, path, body, headers = {}) {
const res = await fetch(BASE + path, {
method,
headers: {
Authorization: `Bearer ${process.env.SCALE_KEY}`,
'Content-Type': 'application/json',
...headers,
},
body: body === undefined ? undefined : JSON.stringify(body),
});
const json = await res.json();
if (!res.ok) {
const e = json.error || {};
throw new Error(`${e.code || res.status}: ${e.message || ''} (${json.request_id || ''})`);
}
return json;
}
async function main() {
// Every connected number, with its customer.
const { numbers } = await scaleApi('GET', '/numbers');
for (const n of numbers) console.log(n.number_id, n.customer_id, n.display_phone_number);
// Send an approved template from the first number.
const sent = await scaleApi('POST', '/messages', {
number_id: numbers[0].number_id,
to: '201012345678',
type: 'template',
template: {
name: 'appointment_reminder',
language: 'ar',
components: [{ type: 'body', parameters: [{ type: 'text', text: 'Mona' }, { type: 'text', text: '5 pm' }] }],
},
}, { 'Idempotency-Key': 'reminder-77' });
console.log(sent.message.id, sent.message.status);
// Usage for the last 30 days — counts only.
const usage = await scaleApi('GET', '/usage');
console.log(usage.totals);
}
main().catch((err) => { console.error(err.message); process.exit(1); });
مسار OTP كامل (Node.js)
// Send a one-time code with an approved AUTHENTICATION template (Meta's category for codes),
// then follow it on your webhook. A template with a copy-code button takes the code twice:
// in the body and in the button (Meta's format).
const crypto = require('crypto');
async function sendOtp(scaleApi, numberId, phone) {
const code = String(crypto.randomInt(100000, 1000000));
// Store a hash of the code with a short expiry in your own database before sending it.
const sent = await scaleApi('POST', '/messages', {
number_id: numberId,
to: phone,
type: 'template',
template: {
name: 'otp_code',
language: 'en_US',
components: [
{ type: 'body', parameters: [{ type: 'text', text: code }] },
{ type: 'button', sub_type: 'url', index: '0', parameters: [{ type: 'text', text: code }] },
],
},
}, { 'Idempotency-Key': `otp-${phone}-${Math.floor(Date.now() / 60000)}` });
return sent.message.id; // match it to message.delivered / message.failed events by data.api_message_id
}
module.exports = { sendOtp };