Homeitop WA Gateway Login dashboard

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

  1. Persiapan di Dashboard Gateway
  2. Konfigurasi
  3. Service Class
  4. Kirim Lewat Queue (Disarankan)
  5. Kirim Media dari File Lokal
  6. Menerima Pesan Masuk (Webhook)
  7. Testing dengan Http::fake
  8. Troubleshooting
  9. Kenapa Device Sering Terputus?

0. Persiapan di Dashboard Gateway

Lakukan sekali, sebelum menulis kode apa pun:

  1. Minta akun ke admin gateway, lalu login di https://wag.homeitop.web.id/login. Pastikan langganan akun masih aktif (kalau kedaluwarsa, API membalas HTTP 403).

  2. Tambah device: di menu Home, tambahkan nomor WhatsApp yang akan dipakai sebagai pengirim (sender), misalnya 6281234567890.

  3. Scan QR: buka device tersebut, lalu scan QR dari HP lewat WhatsApp → Perangkat tertaut (atau pakai pairing code). Tunggu sampai status device menjadi Connected.

  4. Buat API key: menu User → Settings → Generate New API Key, lalu salin nilainya.

  5. (Opsional) Atur webhook: kalau aplikasi Anda perlu menerima pesan masuk, isi URL webhook device (lihat bagian 5).

  6. 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

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:


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.");

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:


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:


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:

  1. 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.
  2. HP utama lama offline. WhatsApp memutus perangkat tertaut kalau HP utama tidak online sekitar 14 hari. Biarkan HP pengirim tetap menyala dan terhubung internet.
  3. Perangkat tertaut dihapus dari menu WhatsApp → Perangkat tertaut di HP.
  4. 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:

Gateway: https://wag.homeitop.web.id