١. نظرة عامة
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://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": { "...": "..." }
}
/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", "...": "..." }
]
}
الأخطاء
| HTTP | code | المعنى |
|---|---|---|
| 400 | missing_credentials | البريد أو كلمة المرور فارغة |
| 401 | invalid_login | البريد أو كلمة المرور غير صحيحة |
| 403 | account_rejected | الحساب مرفوض من الإدارة |
| 429 | too_many_attempts | 5 محاولات خاطئة لنفس البريد — انتظر 15 دقيقة |
| 429 | login_rate_exceeded | أكثر من 30 طلب دخول في الدقيقة من نفس الخادم — انتظر المدة في retry_after |
طلبات /api/login لا تُحسب من رصيد رسائل باقتك، وتعمل حتى لو استهلكت الحد الشهري.
لها سقف مستقل: 30 طلباً في الدقيقة لكل خادم.
/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 }
}
]
}
الأخطاء
| HTTP | code | المعنى |
|---|---|---|
| 401 | missing_credentials | لم تُرسل instance_id وaccess_token |
| 401 | invalid_credentials | بيانات الاعتماد غير صحيحة |
| 403 | subscription_expired | اشتراك الرقم المستخدم منتهٍ أو لم يُفعَّل بعد |
| 429 | numbers_rate_exceeded | أكثر من 30 طلب في الدقيقة لهذا الحساب — انتظر المدة في retry_after |
/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"
/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": "الجلسة متصلة بالفعل، لا حاجة لكود ربط" }
/api/check-number
هل الرقم مسجّل في واتساب؟ استعلام فقط — لا تُرسل أي رسالة للرقم ولا يُحتسب من حد باقتك. الاستعمال الأساسي قبل الربط: تُدخل رقمك فنتأكد أنه رقم واتساب فعلاً قبل أن نعرض لك كود QR، فلا تنتظر مسح كود لرقم لا وجود له أصلاً.
الـ parameters
instance_id | 0001 | مطلوب |
access_token | const0001 | مطلوب |
number | 966501234567 | مطلوب — يُقبل أيضاً 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).
لا تستخدمه لفحص قوائم أرقام.
/api/send
إرسال رسالة نصية. الرقم بصيغة دولية بدون + أو 00 (مثل 966501234567).
الـ body المطلوب
instance_id | 0001 |
access_token | const0001 |
number | 966501234567 |
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
}
/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://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 للمستندات).
/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://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 ومراقبة الاتصال — ضعه في موقعك ليربط عملاؤك واتسابهم بدون تصميم صفحة من الصفر.
أ. الكود الجاهز
انسخ الكود التالي وضعه في صفحة بموقعك (مثل 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 → يعرض الخطأ ويتوقف.
٧. أكواد الأخطاء
| 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 غير مضبوط |
| 400 | missing_credentials | البريد أو كلمة المرور فارغة (login) |
| 401 | invalid_login | البريد أو كلمة المرور غير صحيحة (login) |
| 403 | account_rejected | الحساب مرفوض من الإدارة (login) |
| 429 | too_many_attempts | 5 محاولات خاطئة لنفس البريد — انتظر 15 دقيقة (login) |
| 429 | login_rate_exceeded | تجاوزت 30 طلب دخول في الدقيقة (login) |
| 429 | numbers_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) أو أطول بين كل رسالتين، وتجنّب إرسال نفس النص حرفياً لكل المستلمين (نوّع قليلاً).