Englishابدأ استخدام الـ API
ابدأ استخدام الـ API ←English
انتقل إلى المحتوى
توثيق المطورين

Scale API للمطورين — الدليل والمرجع

من التسجيل إلى أول رسالة: الحساب وأول دفعة، وربط أرقام عملائك، ومفتاح الـAPI، وكل نقاط النهاية وأنواع الرسائل، والـwebhooks، والأخطاء، والأمان، وأمثلة الكود.

في هذه الصفحة11 قسمًا

تتيح منصة 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:**

  • none
  • pending_payment
  • active
  • past_due — خلال مهلة الأيام الثلاثة؛ ما زال الإرسال يعمل.
  • expired
  • cancelled
  • suspended

**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الحقول
texttext.body (≤ 4000)
imageimage.link (رابط https:// عام)، image.caption? (≤ 1000)
videovideo.link‏، video.caption?
audioaudio.link
documentdocument.link‏، document.filename?‏، document.caption?
templatetemplate.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-Idevt_…
X-Scale-Timestampثواني UNIX لهذه المحاولة.
X-Scale-Signaturesha256= + توقيع 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_KEY401لا مفتاح، أو مفتاح بصيغة خاطئة، أو مفتاح في الرابط.
API_KEY_REVOKED401أُلغي، أو استُبدل بمفتاح أحدث.
PAYMENT_REQUIRED402شراء رقم إضافي قبل تأكيد أول دفعة لاشتراكك (§8). ويمكن ربط الرقمين المشمولين قبلها.
NUMBER_SUBSCRIPTION_REQUIRED402أرقامك وروابطك السارية استوفت حصتك: أضف رقماً إضافياً من لوحة التحكم (الاشتراك والفواتير)، أو انتظر انتهاء صلاحية رابط (§8).
PRODUCT_NOT_ENTITLED403مفتاح مساحة عمل على مسار للمطورين، أو مفتاح مطوّر على مسار خاص بمساحات العمل.
SUBSCRIPTION_INACTIVE403الإرسال في وضع الإعداد (قبل أول دفعة)، أو بعد انتهاء المهلة، أو أثناء إيقاف Scale للحساب — يوضح details.mode أيها (setup‏، paused‏، suspended)، مع details.status وdetails.migration‏ (§8، §8.1).
ACCOUNT_RECEIVE_ONLY403حذف حسابك مطلوب: يُرفض الإرسال وإنشاء العملاء وروابط الربط الجديدة حتى يُنفَّذ الحذف أو تلغيه (details.migration‏، §8.1). وتظل الرسائل الواردة تصل إلى الـwebhook.
NUMBER_SUBSCRIPTION_INACTIVE403انتهى اشتراك الرقم الإضافي لهذا الرقم (details.number_id‏، details.subscription_id). جدّده؛ لم يُفصل أي شيء.
WHATSAPP_NOT_CONNECTED403لا يمكن للرقم الإرسال الآن (details.reason). أعد ربطه برابط ربط جديد.
API_ACCESS_DISABLED403أوقفت Scale الـAPI لهذا العميل (مساراته وأحداث الـwebhook الخاصة به). تواصل مع Scale.
INVALID_REQUEST422حقل مفقود أو بصيغة خاطئة (details.field).
INVALID_MESSAGE400 / 422جسم الرسالة غير صالح.
INVALID_PHONE_NUMBER422to ليس رقماً دولياً كاملاً.
INVALID_CUSTOMER422قيمة غير صالحة لـname أو external_ref الخاصين بالعميل.
CUSTOMER_NOT_FOUND404ليس من عملائك.
NUMBER_NOT_FOUND404ليس من أرقامك المربوطة. معرّف رقم الهاتف لدى Meta ليس number_id أبداً.
ONBOARDING_LINK_NOT_FOUND404ليس من روابطك.
MESSAGE_NOT_FOUND404ليس من رسائلك.
WEBHOOK_NOT_FOUND404لم يجد الاختبار أو التحقق أي عنوان: لا للرقم ولا لحسابك.
TEMPLATE_NOT_FOUND404 / 422القالب غير معروف لهذا العميل.
CUSTOMER_NUMBER_MISMATCH422customer_id وnumber_id لا يتطابقان.
CUSTOMER_REF_IN_USE409عميل آخر من عملائك يملك هذه القيمة لـexternal_ref.
CUSTOMER_LIMIT_REACHED409حد للعملاء وضعه مسؤول في Scale على حسابك (لا يوجد حد افتراضياً). تواصل مع Scale لرفعه.
NUMBER_LIMIT_REACHED409الحد الأقصى لأرقام واتساب الذي وضعته Scale لحسابك. تواصل مع Scale لرفعه.
ONBOARDING_LINK_FINISHED409انتهى الرابط ولا يمكن إلغاؤه.
IDEMPOTENCY_CONFLICT409استُخدم Idempotency-Key لطلب مختلف.
OUTSIDE_MESSAGING_WINDOW422رسالة حرة خارج نافذة الـ24 ساعة. أرسل قالباً.
TEMPLATE_NOT_APPROVED422القالب غير معتمد من واتساب.
TEMPLATE_PARAMETER_MISMATCH422عدد خاطئ من متغيّرات الجسم (details.expected‏، details.given).
RATE_LIMITED429تجاوز حد معدّل (انظر §5).
META_API_ERROR502رفض واتساب الرسالة، أو لم يؤكدها (details.outcome).
INTERNAL_ERROR500خطأ من جهة 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. أرسل معرّف Scale number_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 };
أنشئ حساب مطوّر