١. نظرة عامة
C-WTS يوفّر REST API بسيط لإرسال رسائل واتساب نيابةً عن أي عميل مرتبط من لوحة التحكم.
كل عميل لديه instance_id ثابت وaccess_token فريد، يستخدمهما تطبيقك للمصادقة.
خطوات التهيئة (للمبرمج)
- أنشئ العميل من لوحة التحكم: العملاء › إضافة حساب
- افتح صفحة العميل وامسح QR من واتساب → ستصبح الجلسة نشطة
- انسخ
instance_idوaccess_tokenمن جدول العملاء - ضع البيانات في
.envالخاص بـ Laravel كما في القسم 4 - استخدم الـ Service Class لإرسال الرسائل من أي مكان في تطبيقك
٢. المصادقة
كل طلب يحتاج لمعاملين:
| المعامل | المثال | المكان |
|---|---|---|
instance_id | 0001 | query أو body |
access_token | const0001 | query أو body |
الأمان: لا تكشف access_token في الـ frontend أبداً. اجعل كل الطلبات من الـ backend (Laravel).
٣. الـ Endpoints
/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": { "...": "..." }
}
/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"
/api/check-number
هل الرقم مسجّل في واتساب؟ استعلام فقط — لا تُرسل أي رسالة للرقم ولا يُحتسب من حد باقتك. الاستعمال الأساسي قبل الربط: تُدخل رقمك فنتأكد أنه رقم واتساب فعلاً قبل أن نعرض لك كود QR، فلا تنتظر مسح كود لرقم لا وجود له أصلاً.
الـ parameters
instance_id | 0001 | مطلوب |
access_token | const0001 | مطلوب |
number | 966501234567 | مطلوب — يُقبل أيضاً 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: false — ok يعني أن الاستعلام نجح، وexists هو الجواب.
وchecked_by يوضّح من استعلم: client جلستك أنت (بعد الربط)، أو
admin جلسة النظام (قبل الربط، حين لا تكون لك جلسة بعد).
إذا تعذّر الاستعلام
{ "ok": false, "code": "no_session_available", "error": "لا توجد جلسة متصلة تستطيع التحقق من الرقم" }
كل استعلام يمرّ عبر جلسة واتساب حقيقية، وكثرتها من رقم واحد تستفزّ واتساب،
لذا السقف ٣٠ عملية تحقق في الدقيقة لكل عميل (check_rate_exceeded).
لا تستخدمه لفحص قوائم أرقام.
/api/send
إرسال رسالة نصية. الرقم بصيغة دولية بدون + أو 00 (مثل 966501234567).
الـ body المطلوب
instance_id | 0001 |
access_token | const0001 |
number | 966501234567 |
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
}
/api/send-media
إرسال ملف عبر رابط مباشر (URL): صورة أو مستند (PDF/Word…) أو فيديو أو صوت. يُحمَّل الملف من الرابط ويُرسَل عبر نفس طابور الإرسال المتباعد، ويُحتسب كرسالة واحدة من حدّ باقتك.
الـ body المطلوب
instance_id | 0001 | مطلوب |
access_token | const0001 | مطلوب |
number | 966501234567 | مطلوب |
media_url | https://site.com/file.pdf | مطلوب — يبدأ بـ http/https |
type | image / document / video / audio | اختياري — يُستنتج من امتداد الرابط |
caption | تذكير بموعدك | اختياري — نص مرافق (صورة/فيديو/مستند) |
file_name | invoice.pdf | اختياري — اسم المستند الظاهر |
mimetype | application/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 للمستندات).
/api/lookup
يعطيك instance_id وaccess_token الخاصين بالعميل صاحب رقم الجوال —
فيدخل مدير العيادة رقمه في تطبيقه وتُضبط بيانات الاتصال تلقائياً، بدل نسخها يدوياً من
لوحة التحكم إلى ملف .env.
مصادقة مختلفة: هذه النقطة وحدها لا تُصادَق بـ access_token —
فالتوكن هو ما تبحث عنه أصلاً — بل بمفتاح مشترك key تضبطه في
LOOKUP_KEY بملف .env الخاص بالبوابة. وما دام LOOKUP_KEY
فارغاً تبقى النقطة معطّلة تماماً.
وبما أنها تسلّم بيانات اعتماد عميل قائم، لا تُشارك المفتاح إلا مع تطبيقك أنت،
والسقف ١٠ عمليات بحث في الدقيقة لكل عنوان IP.
الـ parameters
key | قيمة LOOKUP_KEY | مطلوب |
phone | 966501234567 | مطلوب — يُقبل أيضاً 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 ومراقبة الاتصال — ضعه في موقعك ليربط عملاؤك واتسابهم بدون تصميم صفحة من الصفر.
أ. الكود الجاهز
انسخ الكود التالي وضعه في صفحة بموقعك (مثل 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 → يعرض الخطأ ويتوقف.
٧. أكواد الأخطاء
| HTTP | code | المعنى |
|---|---|---|
| 200 | — | نجاح |
| 400 | invalid_input | المعاملات ناقصة أو غير صالحة |
| 400 | invalid_number | الرقم قصير جداً أو طويل جداً |
| 400 | invalid_media | رابط الملف غير صالح أو النوع غير مدعوم (send-media) |
| 400 | number_not_registered | الرقم غير مسجّل في WhatsApp |
| 400 | session_not_connected | الجلسة ليست متصلة، اربط QR أولاً |
| 400 | daily_cap_exceeded | تجاوزت الحد اليومي للإرسال، حاول غداً |
| 400 | queue_full | طابور الإرسال ممتلئ مؤقتاً، حاول بعد قليل |
| 400 | timeout | الإرسال استغرق وقتاً أكثر من اللازم |
| 429 | quota_exceeded | تجاوزت الحد الشهري لباقتك |
| 429 | check_rate_exceeded | تجاوزت سقف التحقق من الأرقام في الدقيقة |
| 502 | check_failed | واتساب لم يردّ على استعلام التحقق من الرقم |
| 503 | no_session_available | لا جلستك ولا جلسة النظام متصلة، فتعذّر التحقق من الرقم |
| 401 | missing_credentials | instance_id أو access_token مفقود |
| 401 | invalid_credentials | بيانات الاعتماد غير صحيحة |
| 403 | subscription_expired | انتهى اشتراك العميل |
| 409 | already_connected | الجلسة متصلة بالفعل (عند طلب QR) |
| 202 | qr_not_ready | كود QR لم يُولّد بعد، أعد المحاولة بعد 3-5 ثوانٍ |
| 400 | invalid_phone | رقم البحث ليس 7-15 رقماً (lookup) |
| 401 | invalid_lookup_key | مفتاح البحث بالرقم غير صحيح (lookup) |
| 404 | client_not_found | لا يوجد عميل بهذا الرقم في المنصة (lookup) |
| 429 | lookup_rate_exceeded | تجاوزت سقف البحث بالرقم في الدقيقة |
| 503 | lookup_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) أو أطول بين كل رسالتين، وتجنّب إرسال نفس النص حرفياً لكل المستلمين (نوّع قليلاً).