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

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

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

١. نظرة عامة

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

Base URL الحالي: http://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://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": { "...": "..." }
}
GET /api/qrcode

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

cURL
curl "http://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://c-wts.com/api/qrcode?instance_id=0001&access_token=const0001&force=1"
GET POST /api/check-number

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

الـ parameters
instance_id0001مطلوب
access_tokenconst0001مطلوب
number966501234567مطلوب — يُقبل أيضاً 0501234567
cURL
curl "http://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: falseok يعني أن الاستعلام نجح، و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://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://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://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://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://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');
    }

    /** حالة الجلسة */
    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);

إرسال جماعي (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://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 غير مضبوط

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

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

السبب: الرقم بصيغة خاطئة (يبدأ بـ + أو 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) أو أطول بين كل رسالتين، وتجنّب إرسال نفس النص حرفياً لكل المستلمين (نوّع قليلاً).