Integrasi WA Gateway di Aplikasi Laravel
Panduan ini untuk aplikasi Laravel lain (sisi klien) yang mau mengirim pesan WhatsApp lewat
gateway ini (https://wag.homeitop.web.id) dan menerima pesan masuk lewat webhook. Daftar lengkap endpoint dan
parameternya (send-list, send-poll, send-location, dll.) ada di menu API pada dashboard gateway
(https://wag.homeitop.web.id/api-docs, perlu login). Dokumen ini berisi langkah-langkah memasangnya di Laravel.
Contoh kode ditulis untuk Laravel 8+ (memakai Http facade) dan tetap kompatibel dengan PHP 7.3.
Daftar Isi#
- Persiapan di Dashboard Gateway
- Konfigurasi
- Service Class
- Kirim Lewat Queue (Disarankan)
- Kirim Media dari File Lokal
- Menerima Pesan Masuk (Webhook)
- Testing dengan
Http::fake - Troubleshooting
- Kenapa Device Sering Terputus?
0. Persiapan di Dashboard Gateway#
Lakukan sekali, sebelum menulis kode apa pun:
-
Minta akun ke admin gateway, lalu login di
https://wag.homeitop.web.id/login. Pastikan langganan akun masih aktif (kalau kedaluwarsa, API membalas HTTP403). -
Tambah device: di menu Home, tambahkan nomor WhatsApp yang akan dipakai sebagai pengirim (
sender), misalnya6281234567890. -
Scan QR: buka device tersebut, lalu scan QR dari HP lewat WhatsApp → Perangkat tertaut (atau pakai pairing code). Tunggu sampai status device menjadi Connected.
-
Buat API key: menu User → Settings → Generate New API Key, lalu salin nilainya.
-
(Opsional) Atur webhook: kalau aplikasi Anda perlu menerima pesan masuk, isi URL webhook device (lihat bagian 5).
-
Uji koneksi dari server aplikasi Anda, bukan dari laptop, karena yang harus bisa menjangkau gateway adalah server:
curl -X POST "https://wag.homeitop.web.id/send-message" \ -H "Content-Type: application/json" \ -d '{"api_key":"API_KEY_ANDA","sender":"6281234567890","number":"6281298765432","message":"Tes koneksi"}'Respons yang diharapkan:
{"status":true,"msg":"Message sent successfully!"}.
Yang perlu dicatat untuk langkah berikutnya: URL gateway, API key, dan nomor sender.
1. Konfigurasi#
.env aplikasi Anda:
WA_GATEWAY_URL=https://wag.homeitop.web.id
WA_GATEWAY_API_KEY=XXXX-your-api-key-XXXX
WA_GATEWAY_SENDER=6281234567890
WA_GATEWAY_TIMEOUT=30
WA_GATEWAY_WEBHOOK_SECRET=isi-dengan-string-acak-panjang
WA_GATEWAY_API_KEYdidapat dari dashboard gateway: User → Settings → Generate New API Key.WA_GATEWAY_SENDERadalah nomor device yang sudah di-scan dan berstatusConnecteddi dashboard.WA_GATEWAY_WEBHOOK_SECRETcukup diisi string acak, misalnya hasilopenssl rand -hex 16.
Jangan tertukar dengan
WA_SERVER_KEY. Itu kunci internal antara Laravel gateway dan server Node gateway. Aplikasi klien tidak memerlukannya dan tidak boleh memanggil port Node (3100) atau route/backend-*secara langsung.
config/services.php:
'wa_gateway' => [
'url' => env('WA_GATEWAY_URL'),
'api_key' => env('WA_GATEWAY_API_KEY'),
'sender' => env('WA_GATEWAY_SENDER'),
'timeout' => (int) env('WA_GATEWAY_TIMEOUT', 30),
'webhook_secret' => env('WA_GATEWAY_WEBHOOK_SECRET'),
],
Baca nilainya selalu lewat config(), bukan env(), supaya tetap jalan setelah php artisan config:cache.
2. Service Class#
app/Services/WaGateway.php:
<?php
namespace App\Services;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
class WaGateway
{
public function sendText(string $number, string $message, ?string $sender = null): Response
{
return $this->post('send-message', [
'number' => $number,
'message' => $message,
], $sender);
}
/**
* @param string $mediaType image | video | audio | document
* @param string $url URL publik yang bisa diunduh oleh server gateway
*/
public function sendMedia(
string $number,
string $url,
string $mediaType = 'image',
?string $caption = null,
?string $sender = null
): Response {
return $this->post('send-media', array_filter([
'number' => $number,
'media_type' => $mediaType,
'url' => $url,
'caption' => $caption,
]), $sender);
}
/**
* true = terdaftar, false = tidak terdaftar,
* null = tidak bisa dipastikan (device mati, nomor terlalu pendek, gateway error).
*/
public function checkNumber(string $number, ?string $sender = null): ?bool
{
$res = $this->post('check-number', ['number' => $number], $sender);
return $res->successful() && $res->json('status') === true
? (bool) $res->json('data')
: null;
}
public static function isSuccess(Response $res): bool
{
return $res->successful() && $res->json('status') === true;
}
public function post(string $endpoint, array $payload, ?string $sender = null): Response
{
$config = config('services.wa_gateway');
return Http::baseUrl(rtrim($config['url'], '/'))
->acceptJson()
->timeout($config['timeout'])
->post($endpoint, array_merge([
'api_key' => $config['api_key'],
'sender' => $sender ?? $config['sender'],
], $payload));
}
}
Pemakaian langsung (sinkron):
use App\Services\WaGateway;
$res = app(WaGateway::class)->sendText('081298765432', 'Invoice #123 sudah terbit.');
if (! WaGateway::isSuccess($res)) {
logger()->warning('WA gagal', ['http' => $res->status(), 'msg' => $res->json('msg')]);
}
Catatan:
Httptidak melempar exception untuk respons 4xx/5xx, jadi cek denganisSuccess(). Yang melempar exception (Illuminate\Http\Client\ConnectionException) hanya kegagalan koneksi atau timeout.- Nomor boleh ditulis
08…,62…, atau+62…, karena gateway menormalkannya sendiri. Untuk grup, pakai JID…@g.us. - Gateway menerima banyak tujuan sekaligus (
62812…|62813…). Tapi untuk pengiriman lewat queue, satu job per nomor lebih aman, supaya retry tidak mengirim ulang ke nomor yang sudah berhasil.
3. Kirim Lewat Queue (Disarankan)#
Pengiriman bisa memakan waktu beberapa detik, dan gateway membatasi 60 request/menit. Karena itu, kirim dari job, bukan langsung di dalam request user.
Aturan retry yang dipakai:
| Respons gateway | Tindakan job | Alasan |
|---|---|---|
200 + status: true |
Selesai | Terkirim |
429 |
release() sesuai header Retry-After |
Kena rate limit, coba lagi nanti |
4xx lain (atau 200 + status: false) |
fail() tanpa retry |
Error permanen: nomor tidak terdaftar, api_key/sender salah, device tidak terhubung, parameter salah |
5xx, timeout, koneksi gagal |
Lempar exception, lalu retry | Gangguan sementara di server gateway |
app/Jobs/SendWaMessage.php:
<?php
namespace App\Jobs;
use App\Services\WaGateway;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use RuntimeException;
class SendWaMessage implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 3;
public $backoff = [30, 120];
public $number;
public $message;
public function __construct(string $number, string $message)
{
$this->number = $number;
$this->message = $message;
}
public function handle(WaGateway $wa)
{
$res = $wa->sendText($this->number, $this->message);
if (WaGateway::isSuccess($res)) {
return;
}
if ($res->status() === 429) {
$this->release((int) ($res->header('Retry-After') ?: 60));
return;
}
if ($res->status() < 500) {
$this->fail(new RuntimeException(
"WA ke {$this->number} gagal (HTTP {$res->status()}): " . $res->json('msg', $res->body())
));
return;
}
throw new RuntimeException("WA gateway HTTP {$res->status()}");
}
}
Pemakaian:
SendWaMessage::dispatch($user->phone, "Halo {$user->name}, pesanan Anda sudah dikirim.");
- Jalankan worker:
php artisan queue:work(di produksi pakai Supervisor/PM2). release()karena 429 tetap dihitung sebagai satu percobaan ($tries). Naikkan$trieskalau volume kirim Anda besar.- Kalau job gagal karena
Your device is not connected!, scan ulang device di dashboard gateway, lalu jalankanphp artisan queue:retry alluntuk mengirim ulang job yang gagal. - Untuk broadcast dalam jumlah besar, pakai fitur Campaign di dashboard gateway (ada jeda antar pesan). Kirim massal lewat API berisiko membuat nomor diblokir WhatsApp.
4. Kirim Media dari File Lokal#
/send-media hanya menerima url: tidak ada upload multipart/base64. Server gateway sendiri
yang mengunduh file dari URL tersebut. Kalau file ada di aplikasi Anda, simpan dulu ke disk
public, lalu kirim URL-nya:
use App\Services\WaGateway;
use Illuminate\Support\Facades\Storage;
$path = Storage::disk('public')->putFile('wa-media', $request->file('lampiran'));
$url = Storage::disk('public')->url($path); // memakai APP_URL
app(WaGateway::class)->sendMedia('081298765432', $url, 'document', 'Slip gaji Agustus');
Syarat:
php artisan storage:linksudah dijalankan.APP_URLaplikasi Anda bisa diakses dari server gateway.localhostatau IP LAN tidak akan bisa diunduh oleh gateway yang berada di VPS lain.- File di
wa-media/bisa diakses publik oleh siapa pun yang tahu URL-nya. Jangan simpan dokumen sensitif di sana terlalu lama: hapus setelah terkirim, atau pakai nama file acak (putFilesudah membuat nama acak).
5. Menerima Pesan Masuk (Webhook)#
Atur URL webhook device di dashboard gateway (Home → pilih device → Webhook), misalnya:
https://app-anda.com/api/wa/webhook/isi-dengan-string-acak-panjang
Bagian terakhir URL adalah WA_GATEWAY_WEBHOOK_SECRET. Gateway tidak mengirim tanda tangan atau
header rahasia apa pun, jadi rahasia di path inilah satu-satunya cara menolak request palsu.
Route di routes/api.php. Route api tidak memakai CSRF, jadi tidak perlu pengecualian di
VerifyCsrfToken:
use App\Http\Controllers\WaWebhookController;
Route::post('/wa/webhook/{secret}', WaWebhookController::class)
->withoutMiddleware('throttle:api'); // pesan masuk bisa lebih dari 60/menit
app/Http/Controllers/WaWebhookController.php:
<?php
namespace App\Http\Controllers;
use App\Jobs\ProcessIncomingWa;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class WaWebhookController extends Controller
{
public function __invoke(Request $request, string $secret)
{
abort_unless(hash_equals((string) config('services.wa_gateway.webhook_secret'), $secret), 404);
$data = $request->only(['device', 'message', 'from', 'name', 'participant', 'ppUrl']);
// Media dikirim sebagai array byte (JSON Buffer) di media.stream.data.
// Simpan ke file di sini, supaya payload job tetap kecil.
$data['media_path'] = null;
if ($bytes = $request->input('media.stream.data')) {
$name = preg_replace('/[^A-Za-z0-9._-]/', '_', basename((string) $request->input('media.fileName', 'file')));
$data['media_path'] = 'wa-inbox/' . Str::random(8) . '-' . $name;
$data['media_mimetype'] = $request->input('media.mimetype');
Storage::put($data['media_path'], implode(array_map('chr', $bytes)));
}
ProcessIncomingWa::dispatch($data);
return response('OK', 200); // balas secepatnya; proses berat ada di job
}
}
app/Jobs/ProcessIncomingWa.php (kerangka):
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ProcessIncomingWa implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $data;
public function __construct(array $data)
{
$this->data = $data;
}
public function handle()
{
$isGroup = ! empty($this->data['participant']);
$text = trim((string) ($this->data['message'] ?? ''));
if (! $isGroup && strtolower($text) === 'halo') {
SendWaMessage::dispatch($this->data['from'], "Halo {$this->data['name']}! Ada yang bisa dibantu?");
}
// ... simpan ke database, teruskan ke CS, dll.
}
}
Catatan:
- Gateway tidak melakukan retry webhook. Kalau aplikasi Anda sedang down, pesan masuk itu hilang dari sisi webhook, tapi tetap bisa dilihat di HP/WhatsApp device.
- Body webhook berisi media bisa besar (byte gambar/video dikirim dalam bentuk JSON). Naikkan
client_max_body_size(Nginx) sertapost_max_sizedanmemory_limit(PHP) sesuai kebutuhan. - Untuk pesan grup,
fromberisi JID grup danparticipantberisi nomor pengirimnya. Hati-hati dengan auto-reply di grup. - Jangan balas pesan dari nomor device sendiri (
from === device), supaya tidak terjadi loop.
6. Testing dengan Http::fake#
use App\Jobs\SendWaMessage;
use App\Services\WaGateway;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
public function test_kirim_wa_berhasil()
{
Http::fake([
'*/send-message' => Http::response(['status' => true, 'msg' => 'Message sent successfully!']),
]);
(new SendWaMessage('081298765432', 'Halo'))->handle(app(WaGateway::class));
Http::assertSent(function ($request) {
return Str::endsWith($request->url(), '/send-message')
&& $request['number'] === '081298765432'
&& $request['api_key'] === config('services.wa_gateway.api_key');
});
}
public function test_nomor_tidak_terdaftar_dianggap_gagal()
{
Http::fake([
'*/send-message' => Http::response([
'status' => false,
'msg' => 'Failed to send message! The destination Number not registered in WhatsApp or your sender not connected',
], 400),
]);
$res = app(WaGateway::class)->sendText('081298765432', 'Halo');
$this->assertFalse(WaGateway::isSuccess($res));
$this->assertSame(400, $res->status()); // < 500, jadi job akan fail() tanpa retry
}
Http::fake mencegat semua request keluar, jadi test tidak pernah benar-benar mengirim WhatsApp.
7. Troubleshooting#
Cocokkan msg dengan awalan teksnya, jangan dengan teks persis: teks alasan di belakang
"Failed to send message!" bisa berubah.
| Gejala | Penyebab paling mungkin | Tindakan |
|---|---|---|
Invalid api_key or sender,please check again |
api_key salah, atau sender bukan milik user tersebut |
Cek .env, lalu php artisan config:clear |
Your device is not connected!… |
Device di gateway berstatus terputus | Scan ulang QR di dashboard gateway |
Failed to send message! The destination Number not registered… |
Nomor tujuan tidak punya WhatsApp, atau sesi device mati walau dashboard masih menampilkan Connected |
Cek dengan checkNumber() ke nomor yang pasti aktif. Kalau hasilnya false juga, scan ulang device |
Failed to send message!, Check your connection! |
Server Node gateway mati | Hubungi admin gateway (restart PM2) |
Number is too short to verify… (check-number) |
Nomor ≤ 11 digit | Pakai format internasional lengkap |
HTTP 429 |
Lebih dari 60 request/menit | Kirim lewat queue (bagian 3), atau pakai Campaign |
HTTP 403 |
Langganan user di gateway kedaluwarsa | Hubungi admin gateway |
ConnectionException / timeout |
WA_GATEWAY_URL salah, atau gateway tidak bisa dijangkau dari server Anda |
curl URL gateway dari server aplikasi |
| Media tidak terkirim | URL media tidak bisa diunduh oleh server gateway | Buka URL tersebut dari luar jaringan Anda (bagian 4) |
| Webhook tidak pernah masuk | URL webhook belum diatur, salah secret (404), atau server Anda menolak body besar | Cek log akses web server, lalu uji URL webhook dengan curl -X POST |
Riwayat pengiriman, berhasil maupun gagal beserta alasannya, juga tercatat di menu Message History di dashboard gateway.
8. Kenapa Device Sering Terputus?#
Status device di dashboard berubah menjadi Disconnect setiap kali koneksi gateway ke WhatsApp putus. Yang dilakukan gateway tergantung alasan putusnya:
| Alasan dari WhatsApp | Yang dilakukan gateway | Yang perlu Anda lakukan |
|---|---|---|
| Gangguan jaringan, timeout, server WhatsApp sibuk, restart server gateway | Tersambung ulang otomatis dengan jeda bertahap (2 detik, 4, 8, … maksimal 60 detik). Sesi tidak hilang. | Tidak ada. Kiriman yang gagal selama putus akan di-retry oleh queue job (bagian 3). |
| Logout (401): perangkat tertaut dihapus dari HP, atau sesi dicabut WhatsApp | Sesi dihapus, device menunggu scan QR baru | Scan ulang QR di dashboard |
| Sesi dibuka di server lain (440 conflict) | Berhenti, tidak tersambung ulang | Pastikan nomor ini hanya tersambung di satu server gateway, lalu scan/sambungkan ulang |
| Akun dibatasi/diblokir (403) | Berhenti, tidak tersambung ulang | Cek nomor di aplikasi WhatsApp di HP |
Pemicu logout yang paling sering:
- Nomor yang sama tersambung di dua server gateway sekaligus (misalnya server produksi dan server uji/lokal yang memakai salinan sesi yang sama). Keduanya saling berebut sesi, dan WhatsApp akhirnya me-logout device. Satu nomor = satu server gateway.
- HP utama lama offline. WhatsApp memutus perangkat tertaut kalau HP utama tidak online sekitar 14 hari. Biarkan HP pengirim tetap menyala dan terhubung internet.
- Perangkat tertaut dihapus dari menu WhatsApp → Perangkat tertaut di HP.
- Pengiriman massal. Blast bervolume tinggi lewat API bisa membuat nomor dibatasi WhatsApp. Pakai fitur Campaign (ada jeda antar pesan) dan kirim lewat queue.
Dari sisi aplikasi Laravel Anda:
- Respons
Your device is not connected!…berarti device sedang terputus. Job di bagian 3 menandainya gagal. Setelah device tersambung lagi, kirim ulang denganphp artisan queue:retry all. - Untuk memantau, cek status device secara berkala lewat endpoint
/info-device, lalu beri notifikasi ke admin kalau statusnyaDisconnectlebih dari beberapa menit. Kalau sudah lebih dari ~5 menit tidak kembali sendiri, kemungkinan besar perlu scan ulang.