C-WTS C-WTS برنامج سي للواتس
للمطوّرين

دليل الربط مع Laravel

خطوات تكامل C-WTS مع تطبيق Laravel، مع الأمثلة وأكواد الأخطاء

١. نظرة عامة

C-WTS يوفّر REST API بسيط لإرسال رسائل واتساب نيابةً عن أي عميل مرتبط من لوحة التحكم. كل عميل لديه instance_id ثابت وaccess_token فريد، يستخدمهما تطبيقك للمصادقة.

Base URL الحالي: http://www.c-wts.com

خطوات التهيئة (للمبرمج)

  1. أنشئ العميل من لوحة التحكم: العملاء › إضافة حساب
  2. افتح صفحة العميل وامسح QR من واتساب → ستصبح الجلسة نشطة
  3. انسخ instance_id وaccess_token من جدول العملاء
  4. ضع البيانات في .env الخاص بـ Laravel كما في القسم 4
  5. استخدم الـ Service Class لإرسال الرسائل من أي مكان في تطبيقك

٢. المصادقة

كل طلب يحتاج لمعاملين:

المعاملالمثالالمكان
instance_id0001query أو body
access_tokenconst0001query أو body

الأمان: لا تكشف access_token في الـ frontend أبداً. اجعل كل الطلبات من الـ backend (Laravel).

٣. الـ Endpoints

GET /api/status

يرجع حالة الجلسة الحالية ومعلومات الاشتراك.

status تكون banned حين ترفض واتساب اتصال رقمك مراراً (403) — وهي حالة لا تُصلحها إعادة المحاولة بنفس الرقم، وتفاصيلها في الحقل banned. ترتفع تلقائياً بمجرد نجاح ربط جديد.

cURL
curl "http://www.c-wts.com/api/status?instance_id=0001&access_token=const0001"
الرد عند النجاح
{
  "ok": true,
  "status": "connected",
  "phone": "966501234567",
  "avatar_url": "https://...",
  "platform": "android",
  "banned": null,
  "subscription": {
    "start": 1700000000,
    "end":   1702592000,
    "days_remaining": 25
  }
}
الرد عندما يكون الرقم محظوراً
{
  "ok": true,
  "status": "banned",
  "message": "الرقم محظور من واتساب",
  "detail": "هذا الرقم محظور من واتساب. رفضت واتساب الاتصال مراراً...",
  "banned": {
    "at": 1700000000,
    "phone": "966501234567",
    "message": "هذا الرقم محظور من واتساب..."
  },
  "phone": null,
  "avatar_url": null,
  "platform": null,
  "subscription": { "...": "..." }
}
POST /api/login

تسجيل الدخول ببريد وكلمة مرور لوحة التحكم بدل instance_id وaccess_token. يرجع كل أرقام الحساب مع بيانات اعتماد كل رقم، فتعرض في موقعك صفحة دخول ثم قائمة الأرقام، وبعد اختيار رقم تستخدم instance_id وaccess_token الخاصين به في بقية الطلبات كالمعتاد. بقية الـ Endpoints لم تتغيّر.

is_current تكون false لكل الأرقام هنا، لأنه لم يُختر رقم بعد. ترسل الطلب بـ POST فقط (JSON أو form)، ولا تضع كلمة المرور في الرابط.

الأمان: أرسل هذا الطلب من الـ backend الخاص بموقعك لا من المتصفح، واحفظ بيانات الاعتماد الراجعة في الخادم. بعد 5 محاولات خاطئة لنفس البريد يُرفض الدخول 15 دقيقة (429).

استخدم رابط https مباشرةً. البريد وكلمة المرور في جسم الطلب، ورابط http يُحوَّل إلى https ويسقط معه جسم POST، فيرجع missing_credentials.

cURL
curl -X POST "http://www.c-wts.com/api/login" \
  -H "Content-Type: application/json" \
  -d '{"email":"you@example.com","password":"your-password"}'
الرد عند النجاح
{
  "ok": true,
  "account": { "name": "مؤسسة العجلان", "email": "you@example.com" },
  "count": 2,
  "numbers": [
    {
      "instance_id": "0001",
      "access_token": "f96871d7...426d",
      "name": "مؤسسة العجلان",
      "phone": "966551508655",
      "is_current": false,
      "is_owner": true,
      "status": "connected",
      "message": "متصل",
      "plan": { "id": 2, "name": "الباقة الأساسية" },
      "subscription": { "end": 1702592000, "days_remaining": 25, "expired": false },
      "quota": { "used": 120, "limit": 1000, "remaining": 880 }
    },
    { "instance_id": "0002", "access_token": "c23b69b7...a6e1", "...": "..." }
  ]
}
الأخطاء
HTTPcodeالمعنى
400missing_credentialsالبريد أو كلمة المرور فارغة
401invalid_loginالبريد أو كلمة المرور غير صحيحة
403account_rejectedالحساب مرفوض من الإدارة
429too_many_attempts5 محاولات خاطئة لنفس البريد — انتظر 15 دقيقة
429login_rate_exceededأكثر من 30 طلب دخول في الدقيقة من نفس الخادم — انتظر المدة في retry_after

طلبات /api/login لا تُحسب من رصيد رسائل باقتك، وتعمل حتى لو استهلكت الحد الشهري. لها سقف مستقل: 30 طلباً في الدقيقة لكل خادم.

GET /api/numbers

يرجع كل أرقام واتساب المسجّلة على حسابك. الحقل is_current يساوي true للرقم الذي استُخدمت بيانات اعتماده في هذا الطلب، وis_owner للحساب الرئيسي. حساب برقم واحد يرجع قائمة من عنصر واحد.

يرجع كل رقم مع instance_id وaccess_token الخاصين به، فيمكن لموقعك التبديل بين الأرقام باستخدام بيانات اعتماد الرقم المختار في بقية الطلبات.

الأمان: بيانات اعتماد أي رقم تكشف مفاتيح كل أرقام الحساب. احفظها في الـ backend (مثل ملف .env أو قاعدة بيانات موقعك) ولا ترسلها إلى المتصفح.

قيم status: connected، pending، disconnected، banned، awaiting_activation (رقم مضاف لم يُفعَّل بباقة بعد). للحالة التفصيلية لرقم بعينه (مثل انتظار مسح QR) استخدم /api/status ببيانات اعتماده.

الرقم بحالة awaiting_activation يظهر في القائمة لكن لا يمكن استخدامه بعد: أي طلب ببيانات اعتماده يرجع subscription_expired حتى تفعّله الإدارة بباقة. يُنصح بتعطيل اختياره في موقعك.

طلبات /api/numbers لا تُحسب من رصيد رسائل باقتك، وتعمل حتى لو استهلكت الحد الشهري. لها سقف مستقل: 30 طلباً في الدقيقة لكل حساب (مجموع كل أرقامه).

cURL
curl "http://www.c-wts.com/api/numbers?instance_id=0001&access_token=const0001"
الرد عند النجاح
{
  "ok": true,
  "current_instance_id": "0001",
  "count": 2,
  "numbers": [
    {
      "instance_id": "0001",
      "access_token": "f96871d7...426d",
      "name": "مؤسسة العجلان",
      "phone": "966551508655",
      "is_current": true,
      "is_owner": true,
      "status": "connected",
      "message": "متصل",
      "plan": { "id": 2, "name": "الباقة الأساسية" },
      "subscription": { "end": 1702592000, "days_remaining": 25, "expired": false },
      "quota": { "used": 120, "limit": 1000, "remaining": 880 }
    },
    {
      "instance_id": "0002",
      "access_token": "c23b69b7...a6e1",
      "name": "قاعة ياسمين الشام",
      "phone": "0502025302",
      "is_current": false,
      "is_owner": false,
      "status": "disconnected",
      "message": "غير متصل",
      "plan": { "id": 2, "name": "الباقة الأساسية" },
      "subscription": { "end": 1702592000, "days_remaining": 25, "expired": false },
      "quota": { "used": 0, "limit": 1000, "remaining": 1000 }
    }
  ]
}
الأخطاء
HTTPcodeالمعنى
401missing_credentialsلم تُرسل instance_id وaccess_token
401invalid_credentialsبيانات الاعتماد غير صحيحة
403subscription_expiredاشتراك الرقم المستخدم منتهٍ أو لم يُفعَّل بعد
429numbers_rate_exceededأكثر من 30 طلب في الدقيقة لهذا الحساب — انتظر المدة في retry_after
GET /api/qrcode

يرجع كود QR (base64 PNG) لربط واتساب جديد. يفشل لو كانت الجلسة متصلة بالفعل.

cURL
curl "http://www.c-wts.com/api/qrcode?instance_id=0001&access_token=const0001"
الرد عند النجاح
{ "ok": true, "qr": "data:image/png;base64,iVBORw0KGgo..." }
إذا الكود لم يُولّد بعد (202)
{ "ok": false, "code": "qr_not_ready", "error": "..." }
إذا كان الرقم محظوراً (409)

لا يُولَّد كود لرقم محظور، لأن استطلاع الكود كل ثوانٍ كان سيبقي النظام يصافح واتساب بمصافحات مرفوضة. اعرض للمستخدم رسالة الحظر وزرّاً يعيد الطلب مع force=1 — وهي نيّة صريحة لربط رقم آخر، ترفع العلامة وتولّد كوداً جديداً.

{
  "ok": false,
  "code": "number_banned",
  "error": "هذا الرقم محظور من واتساب...",
  "banned": { "at": 1700000000, "phone": "966501234567" }
}
curl "http://www.c-wts.com/api/qrcode?instance_id=0001&access_token=const0001&force=1"
GET POST /api/pairing-code

بديل عن مسح QR حين يتعذّر: كود من ثمانية أحرف يُدخَل في الهاتف مباشرة (الأجهزة المرتبطة ← ربط جهاز ← الربط برقم الهاتف بدلاً من ذلك). مفيد تحديداً حين يفتح المستخدم الشاشة من الهاتف نفسه فلا يملك جهازاً ثانياً يعرض الكود.

phone بصيغة دولية بلا +. الرقم المكتوب محلياً (05…) يُحوَّل ببادئة الدولة المضبوطة في الخادم، ويعود في الرد بصيغته النهائية — اعرضه للمستخدم قبل الربط. إن أُهمل، يُستعمل رقم الحساب المسجّل.

cURL
curl -X POST "http://www.c-wts.com/api/pairing-code?instance_id=0001&access_token=const0001" \
     -H "Content-Type: application/json" -d '{"phone":"9665xxxxxxxx"}'
الرد عند النجاح
{
  "ok": true,
  "pairing": {
    "code": "VY6C9E5B",
    "pretty": "VY6C-9E5B",
    "phone": "9665xxxxxxxx",
    "expires_at": 1700000180,
    "seconds_left": 180
  }
}

الكود صالح لدقائق معدودة ثم يلزم طلب غيره. طلبٌ جديد يبدأ جلسة نظيفة، فلا تطلبه دورياً — اطلبه عند الحاجة فقط. والكود الساري يظهر أيضاً في /api/status ضمن الحقل pairing.

إذا كانت الجلسة متصلة (409)
{ "ok": false, "code": "already_connected", "error": "الجلسة متصلة بالفعل، لا حاجة لكود ربط" }
GET POST /api/check-number

هل الرقم مسجّل في واتساب؟ استعلام فقط — لا تُرسل أي رسالة للرقم ولا يُحتسب من حد باقتك. الاستعمال الأساسي قبل الربط: تُدخل رقمك فنتأكد أنه رقم واتساب فعلاً قبل أن نعرض لك كود QR، فلا تنتظر مسح كود لرقم لا وجود له أصلاً.

الـ parameters
instance_id0001مطلوب
access_tokenconst0001مطلوب
number966501234567مطلوب — يُقبل أيضاً 0501234567
cURL
curl "http://www.c-wts.com/api/check-number?instance_id=0001&access_token=const0001&number=966501234567"
الرد
{
  "ok": true,
  "number": "966501234567",
  "exists": true,
  "jid": "966501234567@s.whatsapp.net",
  "checked_by": "admin"
}

رقم غير مسجّل ليس خطأ: الرد يبقى ok: true و exists: false — ok يعني أن الاستعلام نجح، وexists هو الجواب. وchecked_by يوضّح من استعلم: client جلستك أنت (بعد الربط)، أو admin جلسة النظام (قبل الربط، حين لا تكون لك جلسة بعد).

إذا تعذّر الاستعلام
{ "ok": false, "code": "no_session_available", "error": "لا توجد جلسة متصلة تستطيع التحقق من الرقم" }

كل استعلام يمرّ عبر جلسة واتساب حقيقية، وكثرتها من رقم واحد تستفزّ واتساب، لذا السقف ٣٠ عملية تحقق في الدقيقة لكل عميل (check_rate_exceeded). لا تستخدمه لفحص قوائم أرقام.

POST /api/send

إرسال رسالة نصية. الرقم بصيغة دولية بدون + أو 00 (مثل 966501234567).

الـ body المطلوب
instance_id0001
access_tokenconst0001
number966501234567
messageنص الرسالة
cURL
curl -X POST "http://www.c-wts.com/api/send" \
  -d "instance_id=0001" \
  -d "access_token=const0001" \
  -d "number=966501234567" \
  -d "message=مرحباً من Laravel"
الرد عند النجاح
{
  "ok": true,
  "message_id": "3EB0F5F0A8F5B26402B11D",
  "timestamp": 1700000000
}
POST /api/send-media

إرسال ملف عبر رابط مباشر (URL): صورة أو مستند (PDF/Word…) أو فيديو أو صوت. يُحمَّل الملف من الرابط ويُرسَل عبر نفس طابور الإرسال المتباعد، ويُحتسب كرسالة واحدة من حدّ باقتك.

الـ body المطلوب
instance_id0001مطلوب
access_tokenconst0001مطلوب
number966501234567مطلوب
media_urlhttps://site.com/file.pdfمطلوب — يبدأ بـ http/https
typeimage / document / video / audioاختياري — يُستنتج من امتداد الرابط
captionتذكير بموعدكاختياري — نص مرافق (صورة/فيديو/مستند)
file_nameinvoice.pdfاختياري — اسم المستند الظاهر
mimetypeapplication/pdfاختياري — للمستندات
cURL — صورة
curl -X POST "http://www.c-wts.com/api/send-media" \
  -d "instance_id=0001" \
  -d "access_token=const0001" \
  -d "number=966501234567" \
  -d "type=image" \
  -d "media_url=https://site.com/photo.jpg" \
  -d "caption=تذكير بموعدك غداً"
cURL — مستند PDF
curl -X POST "http://www.c-wts.com/api/send-media" \
  -d "instance_id=0001" \
  -d "access_token=const0001" \
  -d "number=966501234567" \
  -d "type=document" \
  -d "media_url=https://site.com/invoice.pdf" \
  -d "file_name=فاتورة.pdf"
الرد عند النجاح (تُجدوَل في الطابور)
{
  "ok": true,
  "queued": true,
  "position": 1,
  "eta_seconds": 7,
  "quota": { "used": 12, "limit": 1000, "remaining": 988 }
}

يجب أن يكون media_url رابطاً عاماً يصل إليه الخادم. الحد الأقصى لحجم الملف يتبع حدود واتساب (~100MB للمستندات).

GET POST /api/lookup

يعطيك instance_id وaccess_token الخاصين بالعميل صاحب رقم الجوال — فيدخل مدير العيادة رقمه في تطبيقه وتُضبط بيانات الاتصال تلقائياً، بدل نسخها يدوياً من لوحة التحكم إلى ملف .env.

مصادقة مختلفة: هذه النقطة وحدها لا تُصادَق بـ access_token — فالتوكن هو ما تبحث عنه أصلاً — بل بمفتاح مشترك key تضبطه في LOOKUP_KEY بملف .env الخاص بالبوابة. وما دام LOOKUP_KEY فارغاً تبقى النقطة معطّلة تماماً. وبما أنها تسلّم بيانات اعتماد عميل قائم، لا تُشارك المفتاح إلا مع تطبيقك أنت، والسقف ١٠ عمليات بحث في الدقيقة لكل عنوان IP.

الـ parameters
keyقيمة LOOKUP_KEYمطلوب
phone966501234567مطلوب — يُقبل أيضاً 0501234567
cURL
curl -X POST "http://www.c-wts.com/api/lookup?key=LOOKUP_KEY_HERE" \
  -d "phone=966501234567"
الرد عند وجود العميل
{
  "ok": true,
  "client": {
    "id": 1,
    "name": "عيادة المثال",
    "phone": "0501234567",
    "instance_id": "0001",
    "access_token": "...",
    "status": "connected",
    "subscription": { "end": 1702592000, "days_remaining": 25, "expired": false }
  }
}
إذا لم يوجد عميل بهذا الرقم (404)
{ "ok": false, "code": "client_not_found", "error": "لا يوجد عميل بهذا الرقم في المنصة" }

المطابقة تتجاهل مفتاح الدولة والصفر (تقارن آخر ٩ أرقام)، فـ 0501234567 و966501234567 يجدان العميل نفسه.

٤. كلاس Service جاهز للاستخدام

انسخ الكود التالي في تطبيق Laravel — يوفّر دوال جاهزة لكل العمليات مع التعامل الصحيح مع الأخطاء.

مهمّ عند الإنتاج: استخدم رابطاً بـ https، ومرّر instance_id وaccess_token في الرابط (query string) لا في جسم الطلب. سبب ذلك أن رابط http يُحوَّل تلقائياً إلى https ويسقط معه جسم POST، فتفقد البوابة بيانات الاعتماد وتُرجع missing_credentials. الكلاس أدناه يطبّق ذلك تلقائياً.

أ. أضف القيم في .env

WA_GATEWAY_URL=http://www.c-wts.com
WA_GATEWAY_INSTANCE_ID=0001
WA_GATEWAY_ACCESS_TOKEN=const0001

ب. أضف في config/services.php

'wa_gateway' => [
    'base_url'     => env('WA_GATEWAY_URL', 'http://localhost:3001'),
    'instance_id'  => env('WA_GATEWAY_INSTANCE_ID'),
    'access_token' => env('WA_GATEWAY_ACCESS_TOKEN'),
],

ج. أنشئ الملف app/Services/WaGateway.php

<?php

namespace App\Services;

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

class WaGateway
{
    protected string $baseUrl;
    protected string $instanceId;
    protected string $accessToken;

    public function __construct(?string $instanceId = null, ?string $accessToken = null)
    {
        // مهم: نفرض https — رابط http:// يُحوَّل (redirect) ويسقط معه جسم POST،
        // فتفقد البوابة بيانات الاعتماد وتُرجع missing_credentials.
        $base = preg_replace('#^http://#i', 'https://', config('services.wa_gateway.base_url', 'https://c-wts.com'));
        $this->baseUrl     = rtrim($base, '/');
        $this->instanceId  = $instanceId  ?? config('services.wa_gateway.instance_id');
        $this->accessToken = $accessToken ?? config('services.wa_gateway.access_token');
    }

    /**
     * دخول ببريد وكلمة مرور لوحة التحكم ⇒ كل أرقام الحساب مع بيانات اعتماد كل رقم.
     * بعدها احفظ instance_id و access_token للرقم المختار واستخدمها:
     *   new WaGateway($instanceId, $accessToken)
     */
    public static function login(string $email, string $password): array
    {
        $gw = new static('', '');
        try {
            // كلمة المرور في جسم الطلب لا في الرابط — والرابط https (يفرضه الـ constructor)
            $r = Http::timeout(20)
                ->acceptJson()
                ->asJson()
                ->post("{$gw->baseUrl}/api/login", ['email' => $email, 'password' => $password]);
            return $r->json() ?? ['ok' => false, 'error' => 'Empty response'];
        } catch (\Throwable $e) {
            Log::error('WaGateway login failed', ['msg' => $e->getMessage()]);
            return ['ok' => false, 'code' => 'network_error', 'error' => $e->getMessage()];
        }
    }

    /** كل أرقام الحساب (is_current = الرقم الحالي) مع بيانات اعتماد كل رقم */
    public function numbers(): array
    {
        return $this->get('numbers');
    }

    /** حالة الجلسة */
    public function status(): array
    {
        return $this->get('status');
    }

    /** جلب QR لربط جلسة جديدة */
    public function qrcode(): array
    {
        return $this->get('qrcode');
    }

    /** هل الرقم مسجّل في واتساب؟ استعلام فقط - لا يرسل شيئاً ولا يُحتسب من الباقة */
    public function checkNumber(string $number): array
    {
        return $this->post('check-number', ['number' => $this->cleanNumber($number)]);
    }

    /** إرسال رسالة نصية */
    public function send(string $number, string $message): array
    {
        return $this->post('send', [
            'number'  => $this->cleanNumber($number),
            'message' => $message,
        ]);
    }

    /**
     * إرسال ملف عبر رابط مباشر (صورة/مستند/فيديو/صوت).
     * $type اختياري — يُستنتج من امتداد الرابط لو تُرك null.
     * $opts: ['caption' => ..., 'file_name' => ..., 'mimetype' => ...]
     */
    public function sendMedia(string $number, string $mediaUrl, ?string $type = null, array $opts = []): array
    {
        return $this->post('send-media', array_filter([
            'number'    => $this->cleanNumber($number),
            'media_url' => $mediaUrl,
            'type'      => $type,
            'caption'   => $opts['caption']   ?? null,
            'file_name' => $opts['file_name'] ?? null,
            'mimetype'  => $opts['mimetype']  ?? null,
        ], fn ($v) => $v !== null));
    }

    /** تنظيف الرقم: إزالة + و 00 والمسافات */
    protected function cleanNumber(string $n): string
    {
        $d = preg_replace('/\D+/', '', $n);
        if (str_starts_with($d, '00')) $d = substr($d, 2);
        return $d;
    }

    protected function get(string $endpoint): array
    {
        try {
            $r = Http::timeout(20)
                ->acceptJson()
                ->get("{$this->baseUrl}/api/{$endpoint}", $this->credentials());
            return $r->json() ?? ['ok' => false, 'error' => 'Empty response'];
        } catch (\Throwable $e) {
            Log::error('WaGateway GET failed', ['endpoint' => $endpoint, 'msg' => $e->getMessage()]);
            return ['ok' => false, 'code' => 'network_error', 'error' => $e->getMessage()];
        }
    }

    protected function post(string $endpoint, array $data): array
    {
        try {
            // بيانات الاعتماد تُمرَّر في الرابط (query string) لا في جسم الطلب —
            // فتنجو حتى لو أسقط أي تحويل (redirect) جسم POST.
            $url = "{$this->baseUrl}/api/{$endpoint}?" . http_build_query($this->credentials());
            $r = Http::timeout(30)
                ->acceptJson()
                ->asForm()
                ->post($url, $data);
            return $r->json() ?? ['ok' => false, 'error' => 'Empty response'];
        } catch (\Throwable $e) {
            Log::error('WaGateway POST failed', ['endpoint' => $endpoint, 'msg' => $e->getMessage()]);
            return ['ok' => false, 'code' => 'network_error', 'error' => $e->getMessage()];
        }
    }

    protected function credentials(): array
    {
        return [
            'instance_id'  => $this->instanceId,
            'access_token' => $this->accessToken,
        ];
    }
}

٥. أمثلة استخدام

إرسال رسالة بسيطة

use App\Services\WaGateway;

$wa = new WaGateway();
$res = $wa->send('966501234567', 'مرحباً من Laravel 👋');

if ($res['ok']) {
    return "تم — message_id: {$res['message_id']}";
}
return "فشل: {$res['error']}";

إرسال صورة أو مستند (عبر رابط)

$wa = new WaGateway();

// صورة مع تعليق
$wa->sendMedia('966501234567', 'https://site.com/xray.jpg', 'image', [
    'caption' => 'نتيجة الأشعة',
]);

// مستند PDF باسم ظاهر
$wa->sendMedia('966501234567', 'https://site.com/invoice.pdf', 'document', [
    'file_name' => 'فاتورة-مارس.pdf',
]);

// بدون تحديد النوع — يُستنتج من الرابط تلقائياً
$res = $wa->sendMedia('966501234567', 'https://site.com/clip.mp4');
if (!$res['ok']) {
    Log::warning("فشل إرسال الملف: {$res['error']}");
}

التحقق من حالة الاتصال قبل الإرسال

$wa = new WaGateway();
$status = $wa->status();

if (!$status['ok'] || $status['status'] !== 'connected') {
    return back()->with('error', 'الجلسة غير متصلة، اربط واتساب أولاً');
}

$wa->send($patient->phone, "موعدك في {$appointment->date}");

استخدام عميل مختلف لكل عيادة

// كل عيادة لها instance_id و access_token خاص بها (محفوظين في DB)
$wa = new WaGateway($clinic->instance_id, $clinic->access_token);
$wa->send($patient->phone, $message);

صفحة دخول في موقعك واختيار الرقم (حساب بأكثر من رقم)

// ١. مرة واحدة: المستخدم يدخل بريد وكلمة مرور لوحة التحكم
$res = WaGateway::login($request->email, $request->password);
if (! $res['ok']) {
    return back()->withErrors($res['error']);
}
// اعرض $res['numbers'] ليختار منها (عطّل ما حالته awaiting_activation)

// ٢. بعد الاختيار: احفظ بيانات الرقم المختار في قاعدة بيانات موقعك
$picked = collect($res['numbers'])->firstWhere('instance_id', $request->instance_id);
$user->update([
    'wa_instance_id'  => $picked['instance_id'],
    'wa_access_token' => $picked['access_token'],
]);

// ٣. بعدها بلا دخول: القائمة المحدّثة والرقم الحالي من البيانات المحفوظة
$wa   = new WaGateway($user->wa_instance_id, $user->wa_access_token);
$list = $wa->numbers();          // is_current = الرقم المحفوظ حالياً
$wa->send($patient->phone, $message);

// التبديل لرقم آخر = حفظ instance_id و access_token لذلك الرقم من $list['numbers']

إرسال جماعي (Broadcast)

foreach ($patients as $p) {
    $res = $wa->send($p->phone, "تذكير بالموعد غداً");
    if (!$res['ok']) {
        Log::warning("فشل إرسال لـ {$p->phone}: {$res['error']}");
    }
    usleep(500_000); // نصف ثانية بين كل رسالة (لتفادي الحظر)
}

٦. صفحة الربط الجاهزة (Embed HTML)

قالب HTML كامل ذاتي الاحتواء (HTML + CSS + JS في ملف واحد) لعرض QR ومراقبة الاتصال — ضعه في موقعك ليربط عملاؤك واتسابهم بدون تصميم صفحة من الصفر.

تنزيل سريع: wa-connect-template.html

أ. الكود الجاهز

انسخ الكود التالي وضعه في صفحة بموقعك (مثل connect.html). استبدل INSTANCE_ID_HERE وACCESS_TOKEN_HERE ببيانات اعتماد عميلك، أو اجلبها ديناميكياً من backend.

<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>اربط واتسابك</title>
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;600;700;800&display=swap" rel="stylesheet">
<style>
  * { box-sizing: border-box; margin: 0; padding: 0; }
  body { font-family: 'Cairo', sans-serif; background: linear-gradient(135deg,#f0fdf4,#ecfdf5); min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; color: #1f2937; }
  .wa-emb { background: #fff; border-radius: 16px; box-shadow: 0 20px 50px rgba(0,0,0,.1); padding: 32px; max-width: 480px; width: 100%; text-align: center; }
  .wa-emb-icon { width: 64px; height: 64px; border-radius: 50%; background: linear-gradient(135deg,#25d366,#128c7e); color: #fff; display: inline-flex; align-items: center; justify-content: center; font-size: 32px; margin: 0 auto 16px; }
  .wa-emb h1 { font-size: 22px; font-weight: 800; margin-bottom: 6px; }
  .wa-emb .sub { color: #6b7280; font-size: 14px; margin-bottom: 24px; }
  .wa-emb-stage { min-height: 320px; display: flex; flex-direction: column; align-items: center; justify-content: center; }
  .wa-emb-spinner { width: 40px; height: 40px; border: 3px solid #e5e7eb; border-top-color: #16a34a; border-radius: 50%; animation: wa-spin 1s linear infinite; }
  @keyframes wa-spin { to { transform: rotate(360deg); } }
  .wa-emb-qr { padding: 14px; background: #fff; border: 1px solid #e5e7eb; border-radius: 12px; box-shadow: 0 6px 20px rgba(0,0,0,.06); }
  .wa-emb-qr img { width: 240px; height: 240px; display: block; }
  .wa-emb-steps { text-align: right; margin: 16px 0; padding: 14px; background: #f5f3ff; border-radius: 10px; font-size: 13.5px; line-height: 2; color: #374151; list-style: none; counter-reset: s; }
  .wa-emb-steps li::before { content: counter(s) '. '; counter-increment: s; color: #6366f1; font-weight: 700; }
  .wa-emb-status { display: inline-flex; align-items: center; gap: 6px; padding: 5px 14px; border-radius: 999px; font-size: 12.5px; font-weight: 700; margin-bottom: 12px; }
  .wa-emb-status.ok { background: #d1fae5; color: #16a34a; }
  .wa-emb-status.wait { background: #fef3c7; color: #d97706; }
  .wa-emb-phone { font-size: 22px; font-weight: 800; direction: ltr; margin-top: 8px; }
  .wa-emb-success { color: #16a34a; }
  .wa-emb-success svg { width: 64px; height: 64px; margin-bottom: 12px; }
  .wa-emb-err { color: #dc2626; padding: 14px; background: #fef2f2; border-radius: 10px; font-size: 13px; }
  .wa-emb-foot { margin-top: 18px; font-size: 11.5px; color: #9ca3af; }
</style>
</head>
<body>

<div class="wa-emb">
  <div class="wa-emb-icon">📱</div>
  <h1>اربط حساب واتساب</h1>
  <p class="sub">امسح كود QR من هاتفك لتفعيل الإرسال الآلي</p>

  <div id="wa-stage" class="wa-emb-stage">
    <div class="wa-emb-spinner"></div>
    <p style="margin-top:14px;color:#6b7280;font-size:13px">جاري تحميل الكود...</p>
  </div>

  <p class="wa-emb-foot">مدعوم من C-WTS</p>
</div>

<script>
(function () {
  var API = "http://www.c-wts.com";
  var INSTANCE_ID  = "INSTANCE_ID_HERE";
  var ACCESS_TOKEN = "ACCESS_TOKEN_HERE";
  var stage = document.getElementById('wa-stage');
  var lastQr = null, lastView = null, lastNotice = null;

  function showSpinner(msg) {
    lastNotice = null;
    stage.innerHTML = '<div class="wa-emb-spinner"></div><p style="margin-top:14px;color:#6b7280;font-size:13px">' + msg + '</p>';
  }
  // إشعار سبب التعليق: أثناء إعادة المحاولة نُبقي السبينر مع رسالة تحذير، وإلا نعرض صندوق خطأ ثابت
  function showNotice(msg, retrying) {
    if (msg === lastNotice) return;
    lastNotice = msg;
    stage.innerHTML = retrying
      ? '<div class="wa-emb-spinner"></div><p style="margin-top:14px;color:#d97706;font-size:13px">' + msg + '</p>'
      : '<div class="wa-emb-err">⚠ ' + msg + '</div>';
  }
  function showQR(qr) {
    if (qr === lastQr) return;
    lastQr = qr; lastNotice = null;
    stage.innerHTML =
      '<div class="wa-emb-status wait">⏳ في انتظار المسح</div>' +
      '<ol class="wa-emb-steps">' +
        '<li>افتح <strong>واتساب</strong> على هاتفك</li>' +
        '<li>اذهب للإعدادات → <strong>الأجهزة المرتبطة</strong></li>' +
        '<li>اضغط <strong>ربط جهاز</strong> ثم وجّه الكاميرا للكود</li>' +
      '</ol>' +
      '<div class="wa-emb-qr"><img src="' + qr + '" alt="QR"></div>';
  }
  function showConnected(phone) {
    stage.innerHTML =
      '<div class="wa-emb-success">' +
        '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M22 11.08V12a10 10 0 11-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>' +
        '<h2 style="font-size:18px;margin-bottom:6px">تم الربط بنجاح!</h2>' +
        (phone ? '<div class="wa-emb-phone">+' + phone + '</div>' : '') +
        '<p style="margin-top:10px;color:#6b7280;font-size:13.5px">حسابك جاهز لاستقبال طلبات الإرسال.</p>' +
      '</div>';
  }
  function showError(msg) {
    stage.innerHTML = '<div class="wa-emb-err">⚠ ' + msg + '</div>';
  }

  function poll() {
    var url = API + '/api/qrcode?instance_id=' + encodeURIComponent(INSTANCE_ID) + '&access_token=' + encodeURIComponent(ACCESS_TOKEN);
    fetch(url, { cache: 'no-store' })
      .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, status: r.status, body: j }; }); })
      .then(function (res) {
        if (res.body.ok && res.body.qr) {
          if (lastView !== 'qr') lastView = 'qr';
          showQR(res.body.qr);
        } else if (res.status === 409) {
          if (lastView !== 'connected') {
            lastView = 'connected';
            fetch(API + '/api/status?instance_id=' + INSTANCE_ID + '&access_token=' + ACCESS_TOKEN)
              .then(function (r) { return r.json(); })
              .then(function (s) { showConnected(s.phone); });
          }
        } else if (res.status === 202) {
          if (res.body.code === 'connection_problem') {
            lastView = 'notice'; lastQr = null;
            showNotice(res.body.error, res.body.retrying !== false);
          } else if (lastView !== 'wait') {
            lastView = 'wait'; showSpinner('جاري توليد الكود... خلال ثوانٍ');
          }
        } else if (res.status === 401 || res.status === 403) {
          showError(res.body.error || 'بيانات اعتماد غير صحيحة');
          return;
        } else {
          showError(res.body.error || 'حدث خطأ غير متوقع');
        }
        setTimeout(poll, 3000);
      })
      .catch(function (e) {
        showError('فشل الاتصال بالخادم. أعد المحاولة قريباً.');
        setTimeout(poll, 5000);
      });
  }
  poll();
})();
</script>

</body>
</html>

ب. مثال Laravel Blade (يحقن البيانات تلقائياً)

لو عندك multi-tenant: حقن قيم العميل من backend مباشرة في الـ HTML.

{{-- resources/views/wa-connect.blade.php --}}
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
    <meta charset="UTF-8">
    <title>اربط واتساب — {{ $clinic->name }}</title>
    {{-- ... باقي الـ CSS من القالب أعلاه ... --}}
</head>
<body>

<div class="wa-emb">
    <h1>اربط حساب واتساب لـ {{ $clinic->name }}</h1>
    <div id="wa-stage" class="wa-emb-stage">
        <div class="wa-emb-spinner"></div>
    </div>
</div>

<script>
(function () {
    var API          = @json(config('services.wa_gateway.base_url'));
    var INSTANCE_ID  = @json($clinic->wa_instance_id);
    var ACCESS_TOKEN = @json($clinic->wa_access_token);
    // ... باقي الـ JS من القالب أعلاه ...
})();
</script>

</body>
</html>

ج. مثال Controller في Laravel

// app/Http/Controllers/WaConnectController.php
public function show(Clinic $clinic)
{
    return view('wa-connect', compact('clinic'));
}

// routes/web.php
Route::get('/clinic/{clinic}/wa-connect', [WaConnectController::class, 'show'])
    ->name('wa.connect');

د. كيف يعمل القالب؟

  • عند تحميل الصفحة، يستدعي GET /api/qrcode كل 3 ثوانٍ.
  • إذا رجع QR → يعرضه مع خطوات المسح.
  • إذا رجع 409 (متصل بالفعل) → يستدعي /api/status ويعرض شاشة "تم الربط بنجاح" بالرقم.
  • إذا رجع 202 (لم يُولّد بعد) → يعرض spinner ويعيد المحاولة.
  • إذا رجع 401/403 → يعرض الخطأ ويتوقف.

٧. أكواد الأخطاء

HTTPcodeالمعنى
200—نجاح
400invalid_inputالمعاملات ناقصة أو غير صالحة
400invalid_numberالرقم قصير جداً أو طويل جداً
400invalid_mediaرابط الملف غير صالح أو النوع غير مدعوم (send-media)
400number_not_registeredالرقم غير مسجّل في WhatsApp
400session_not_connectedالجلسة ليست متصلة، اربط QR أولاً
400daily_cap_exceededتجاوزت الحد اليومي للإرسال، حاول غداً
400queue_fullطابور الإرسال ممتلئ مؤقتاً، حاول بعد قليل
400timeoutالإرسال استغرق وقتاً أكثر من اللازم
429quota_exceededتجاوزت الحد الشهري لباقتك
429check_rate_exceededتجاوزت سقف التحقق من الأرقام في الدقيقة
502check_failedواتساب لم يردّ على استعلام التحقق من الرقم
503no_session_availableلا جلستك ولا جلسة النظام متصلة، فتعذّر التحقق من الرقم
401missing_credentialsinstance_id أو access_token مفقود
401invalid_credentialsبيانات الاعتماد غير صحيحة
403subscription_expiredانتهى اشتراك العميل
409already_connectedالجلسة متصلة بالفعل (عند طلب QR)
202qr_not_readyكود QR لم يُولّد بعد، أعد المحاولة بعد 3-5 ثوانٍ
400invalid_phoneرقم البحث ليس 7-15 رقماً (lookup)
401invalid_lookup_keyمفتاح البحث بالرقم غير صحيح (lookup)
404client_not_foundلا يوجد عميل بهذا الرقم في المنصة (lookup)
429lookup_rate_exceededتجاوزت سقف البحث بالرقم في الدقيقة
503lookup_disabledالبحث بالرقم غير مفعّل — LOOKUP_KEY غير مضبوط
400missing_credentialsالبريد أو كلمة المرور فارغة (login)
401invalid_loginالبريد أو كلمة المرور غير صحيحة (login)
403account_rejectedالحساب مرفوض من الإدارة (login)
429too_many_attempts5 محاولات خاطئة لنفس البريد — انتظر 15 دقيقة (login)
429login_rate_exceededتجاوزت 30 طلب دخول في الدقيقة (login)
429numbers_rate_exceededتجاوزت 30 طلب لقائمة الأرقام في الدقيقة (numbers)

٨. أخطاء شائعة وحلولها

الرسالة لا تُرسل ولا يظهر خطأ

السبب: الرقم بصيغة خاطئة (يبدأ بـ + أو 00 أو فيه مسافات).

الحل: الـ Service Class يُنظّف الرقم تلقائياً، لكن تأكّد من تمرير رقم بصيغة دولية كاملة.

session_not_connected

السبب: العميل لم يمسح QR، أو فُصلت الجلسة.

الحل: ادخل لوحة التحكم → افتح صفحة العميل → امسح QR من جديد.

number_not_registered

السبب: الرقم غير مسجّل في WhatsApp.

الحل: تأكد من صحة الرقم وكود الدولة. الـ API يفحص أولاً قبل الإرسال.

subscription_expired

السبب: انتهت مدة اشتراك هذا العميل.

الحل: جدّد الاشتراك من لوحة التحكم → أيقونة 🔄 الخضراء بجانب اسم العميل.

أول رسالة لرقم جديد بطيئة (5-15 ثانية)

السبب: Baileys يبني encryption keys مع المستلم لأول مرة.

الحل: طبيعي. الرسائل التالية لنفس الرقم فورية. زد timeout في Http إلى 30 ثانية على الأقل.

الإرسال الجماعي يتم حظره

السبب: WhatsApp يكتشف نمط إرسال آلي.

الحل: ضع usleep(500_000) أو أطول بين كل رسالتين، وتجنّب إرسال نفس النص حرفياً لكل المستلمين (نوّع قليلاً).