١. نظرة عامة
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
يرجع حالة الجلسة الحالية ومعلومات الاشتراك.
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",
"subscription": {
"start": 1700000000,
"end": 1702592000,
"days_remaining": 25
}
}
/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": "..." }
/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
}
٤. كلاس Service جاهز للاستخدام
انسخ الكود التالي في تطبيق Laravel — يوفّر دوال جاهزة لكل العمليات مع التعامل الصحيح مع الأخطاء.
أ. أضف القيم في .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)
{
$this->baseUrl = rtrim(config('services.wa_gateway.base_url'), '/');
$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 send(string $number, string $message): array
{
return $this->post('send', [
'number' => $this->cleanNumber($number),
'message' => $message,
]);
}
/** تنظيف الرقم: إزالة + و 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 {
$r = Http::timeout(30)
->acceptJson()
->asForm()
->post("{$this->baseUrl}/api/{$endpoint}", array_merge($this->credentials(), $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();
$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;
function showSpinner(msg) {
stage.innerHTML = '<div class="wa-emb-spinner"></div><p style="margin-top:14px;color:#6b7280;font-size:13px">' + msg + '</p>';
}
function showQR(qr) {
if (qr === lastQr) return;
lastQr = qr;
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 (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 | number_not_registered | الرقم غير مسجّل في WhatsApp |
| 400 | session_not_connected | الجلسة ليست متصلة، اربط QR أولاً |
| 400 | timeout | الإرسال استغرق وقتاً أكثر من اللازم |
| 401 | missing_credentials | instance_id أو access_token مفقود |
| 401 | invalid_credentials | بيانات الاعتماد غير صحيحة |
| 403 | subscription_expired | انتهى اشتراك العميل |
| 409 | already_connected | الجلسة متصلة بالفعل (عند طلب QR) |
| 202 | qr_not_ready | كود QR لم يُولّد بعد، أعد المحاولة بعد 3-5 ثوانٍ |
٨. أخطاء شائعة وحلولها
الرسالة لا تُرسل ولا يظهر خطأ
السبب: الرقم بصيغة خاطئة (يبدأ بـ + أو 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) أو أطول بين كل رسالتين، وتجنّب إرسال نفس النص حرفياً لكل المستلمين (نوّع قليلاً).