Tutorial step-by-step bikin Google Sheet jadi panel broadcast WA via Apps Script + API broadcast/create Alat WA.
Tutorial ini akan menunjukkan cara mengubah Google Sheet biasa menjadi panel broadcast WhatsApp. Tim sales/marketing tidak perlu masuk ke dashboard Alat WA - cukup isi data di Sheet, klik tombol, broadcast langsung jalan. Plus bisa pantau status real-time per baris (Sent/Delivered/Read).
Satu Google Sheet dengan kemampuan:
Buka sheets.new. Header minimal yang dibutuhkan: phone dan message. Kolom status dan broadcast_id opsional - akan auto-isi.
| A: phone | B: message | C: status | D: broadcast_id |
|---|---|---|---|
| 08123456789 | Halo Budi, ada promo Lebaran 30%! Cek toko kami ya kak. | ||
| 08987654321 | Hai Sari, lagi diskon 30% nih buat Lebaran. Stok terbatas. |
Penting: Pesan di kolom message harus sudah final - persis seperti yang akan diterima customer. Tidak ada placeholder yang akan diproses script. Kalau mau personalisasi pakai formula Sheet:
="Halo "&C2&", ada promo Lebaran 30%!"
Kalau kamu upload file .xlsx ke Drive, file akan terbuka dengan badge .XLSX di sebelah judul - menu Extensions/Ekstensi disembunyikan! Klik File -> Save as Google Sheets dulu untuk konversi ke format native. Setelah jadi Google Sheets, badge hilang dan menu Ekstensi muncul.
Code.gs// ============== KONFIGURASI ==============
const ALATWA_URL = 'https://api.alatwa.com/broadcast/create';
const STATUS_URL = 'https://api.alatwa.com/broadcast/status/';
const API_KEY = 'PASTE_API_KEY_DISINI'; // dari Profile -> API Key
const DEVICE = 'YOUR_DEVICE_ID'; // Device ID dari menu Device (BUKAN nomor WA)
const TITLE = 'Broadcast dari Sheet';
const INTERVAL_FROM = 5; // jeda min antar pesan (detik)
const INTERVAL_TO = 15; // jeda max antar pesan (detik)
const SHEET_NAME = 'Sheet1'; // ganti kalau sheet kamu nama lain
const COL_PHONE = 1;
const COL_MESSAGE = 2;
const COL_STATUS = 3;
const COL_BC_ID = 4;
// ============== MENU ==============
function onOpen() {
SpreadsheetApp.getUi()
.createMenu('Alat WA')
.addItem('Kirim Broadcast', 'kirimBroadcast')
.addItem('Refresh Status Per Baris', 'refreshStatusPerBaris')
.addItem('Cek Status Ringkasan', 'cekStatusTerakhir')
.addToUi();
}
function ui_(msg) { SpreadsheetApp.getUi().alert(msg); }
// ============== KIRIM BROADCAST ==============
function kirimBroadcast() {
const sh = SpreadsheetApp.getActive().getSheetByName(SHEET_NAME);
const lastRow = sh.getLastRow();
if (lastRow < 2) { ui_('Sheet kosong.'); return; }
const data = sh.getRange(2, 1, lastRow - 1, 2).getValues();
const receiver = [];
const rowIndices = [];
data.forEach((row, i) => {
if (!row[0] || !row[1]) return;
receiver.push({ phone: String(row[0]).trim(), message: String(row[1]) });
rowIndices.push(i + 2);
});
if (!receiver.length) { ui_('Tidak ada baris valid.'); return; }
const payload = {
device: DEVICE,
title: TITLE + ' - ' + new Date().toLocaleString('id-ID'),
date_execute: 'now',
interval_from: INTERVAL_FROM,
interval_to: INTERVAL_TO,
type: 'text',
generate_receiver: 'yes',
receiver: receiver,
};
const res = UrlFetchApp.fetch(ALATWA_URL, {
method: 'post',
contentType: 'application/json',
headers: { 'Authorization': API_KEY },
payload: JSON.stringify(payload),
muteHttpExceptions: true,
});
const json = JSON.parse(res.getContentText() || '{}');
if (json.status === 'ok') {
rowIndices.forEach(idx => {
sh.getRange(idx, COL_STATUS).setValue('Scheduled');
sh.getRange(idx, COL_BC_ID).setValue(json.id);
});
PropertiesService.getDocumentProperties().setProperty('LAST_BC_ID', json.id);
ui_('SUKSES! Broadcast dibuat.
ID: ' + json.id + '
Total: ' + receiver.length + ' nomor
Klik "Refresh Status Per Baris" beberapa detik kemudian untuk update status real-time.');
} else {
ui_('GAGAL: ' + (json.message || 'unknown'));
}
}
// ============== REFRESH STATUS PER BARIS ==============
function refreshStatusPerBaris() {
const sh = SpreadsheetApp.getActive().getSheetByName(SHEET_NAME);
const lastRow = sh.getLastRow();
if (lastRow < 2) { ui_('Sheet kosong.'); return; }
const data = sh.getRange(2, 1, lastRow - 1, 4).getValues();
const ids = [...new Set(data.map(r => String(r[3] || '').trim()).filter(Boolean))];
if (!ids.length) { ui_('Belum ada broadcast_id di kolom D.'); return; }
const phoneStatus = {};
ids.forEach(bcId => {
const res = UrlFetchApp.fetch(STATUS_URL + bcId, {
method: 'get',
headers: { 'Authorization': API_KEY },
muteHttpExceptions: true,
});
const json = JSON.parse(res.getContentText() || '{}');
if (json.status !== 'ok' || !Array.isArray(json.data)) return;
json.data.forEach(rec => {
let p = String(rec.receiver || '').replace(/D/g, '');
if (p.indexOf('0') === 0) p = '62' + p.substring(1);
phoneStatus[p] = rec.status;
});
});
let updated = 0;
data.forEach((row, i) => {
let phone = String(row[0] || '').replace(/D/g, '');
if (phone.indexOf('0') === 0) phone = '62' + phone.substring(1);
if (phone && phoneStatus[phone]) {
sh.getRange(i + 2, COL_STATUS).setValue(phoneStatus[phone]);
updated++;
}
});
ui_('Status di-update di ' + updated + ' baris.
Legenda:
- Scheduled: antri
- Sent: keluar dari engine
- Delivered: sampai HP (centang abu)
- Read: dibaca (centang biru)
- Failed: gagal');
}
// ============== CEK STATUS RINGKASAN ==============
function cekStatusTerakhir() {
const id = PropertiesService.getDocumentProperties().getProperty('LAST_BC_ID');
if (!id) { ui_('Belum pernah kirim broadcast.'); return; }
const res = UrlFetchApp.fetch(STATUS_URL + id, {
method: 'get',
headers: { 'Authorization': API_KEY },
muteHttpExceptions: true,
});
const json = JSON.parse(res.getContentText() || '{}');
if (json.status !== 'ok' || !json.summary) { ui_('Status tidak tersedia.'); return; }
const s = json.summary;
ui_(
'Status Broadcast Terakhir
' +
'ID: ' + id + '
' +
'Total : ' + (s.total || 0) + '
' +
'Pending : ' + (s.scheduled || 0) + '
' +
'Sent : ' + (s.sent || 0) + '
' +
'Delivered: ' + (s.delivered || 0) + '
' +
'Read : ' + (s.read || 0) + '
' +
'Failed : ' + (s.failed || 0)
);
}
API_KEY -> paste API Key dari step 1DEVICE -> Device ID dari menu DeviceScheduled, kolom D terisi broadcast_idSent -> Delivered -> Read sesuai progress di HP penerimaAPI /broadcast/status/{id} return data per-recipient lengkap dengan status terkini:
{
"status": "ok",
"id": "9a2c8b7d3e1f5a6c8d9e2f1b3a4c5d6e",
"summary": {
"total": "100", "scheduled": "25", "sent": "75",
"delivered": "70", "read": "50", "failed": "5", "stop": "0"
},
"data": [
{
"message_id": "BAE52XXXXXXXX",
"receiver": "6281234567891",
"status": "Read",
"date_sent": "2024-05-28 14:15:53"
}
]
}
Script refreshStatusPerBaris baca array data, match by receiver phone, lalu update kolom status di Sheet.
| Status | Arti |
|---|---|
| Scheduled | Antri di queue, belum dikirim |
| Sent | Engine sudah kirim ke server WhatsApp (1 centang abu) |
| Delivered | Sampai HP penerima (2 centang abu) |
| Read | Sudah dibaca (2 centang biru) |
| Failed | Gagal kirim (nomor invalid / blokir / dll) |
| Stop | Dihentikan manual dari dashboard |
Karena setiap baris di Sheet adalah pesan independen, bebas variasikan kalimat per baris. Atau pakai spintax di message - server otomatis pilih random:
{Halo|Hai|Hi} kak, {ada promo|lagi diskon|sale} Lebaran 30%!
Apps Script -> Triggers (icon jam) -> Add Trigger
refreshStatusPerBarisTrigger: function kirimBroadcast, time-driven Daily / Weekly.
Tambah kolom helper name di kolom E. Di kolom message pakai formula:
="Halo "&E2&", ada promo Lebaran 30% untuk semua produk!"
| Masalah | Solusi |
|---|---|
| Menu Extensions tidak muncul | File masih mode .XLSX. File -> Save as Google Sheets dulu. |
Unauthorized | API Key salah - cek Profile -> API Key |
Unknown device | Device ID salah / disconnect - cek menu Device, scan ulang QR |
| Pesan tidak masuk meski OK | Nomor invalid - script sudah auto-format dengan generate_receiver: yes |
| Apps Script timeout | List >2000 - pecah jadi beberapa batch |
| Customer komplain spam | Naikkan INTERVAL_TO ke 30-60 detik |
Terakhir diperbarui: 25 Apr 2026
Tanpa server, tanpa coding. Daftar, scan QR WhatsApp, dan auto reply pertamamu bisa langsung jalan.