WhatsApp API untuk Broadcast & Bulk Message
Panduan broadcast dan bulk message dengan WhatsApp API. Rate limiting, best practices, avoid ban. Tutorial lengkap developer!
Bulk message perlu strategi!
Kirim pesan ke banyak orang via WhatsApp API perlu memperhatikan rate limit, template rules, dan anti-spam untuk menghindari ban.
Official vs Unofficial
📊 PERBANDINGAN:
OFFICIAL (Cloud API):
✅ Legal & aman
✅ Template-based
✅ High throughput
❌ Harus pakai HSM templates
❌ Biaya per conversation
UNOFFICIAL (Library):
⚠️ Risiko ban
⚠️ Rate limit ketat
✅ Flexible content
✅ Gratis
❌ Tidak stabilRate Limits
Official API:
📊 RATE LIMIT OFFICIAL:
MESSAGING LIMITS (per 24 jam):
- Tier 1: 1,000 business-initiated
- Tier 2: 10,000 business-initiated
- Tier 3: 100,000 business-initiated
- Tier 4: Unlimited
MESSAGES PER SECOND:
- Standard: 80 messages/second
- High throughput: 250+ msg/second
CARA NAIK TIER:
- Maintain quality rating
- Tidak banyak block/report
- Volume konsistenUnofficial API:
⚠️ RATE LIMIT UNOFFICIAL:
RECOMMENDED SAFE:
- 1 pesan per 3-5 detik
- Max 200-300 pesan per hari
- Jeda 1 jam setiap 50 pesan
RISIKO BAN:
- Terlalu cepat = temporary ban
- Banyak report = permanent ban
- Konten spam = instant banImplementasi Broadcast
Official API Broadcast:
javascript
const axios = require('axios');
const PHONE_NUMBER_ID = process.env.PHONE_NUMBER_ID;
const ACCESS_TOKEN = process.env.ACCESS_TOKEN;
async function sendTemplateMessage(to, templateName, templateParams) {
const url = `https://graph.facebook.com/v17.0/${PHONE_NUMBER_ID}/messages`;
const payload = {
messaging_product: 'whatsapp',
to: to,
type: 'template',
template: {
name: templateName,
language: { code: 'id' },
components: templateParams
}
};
try {
const response = await axios.post(url, payload, {
headers: {
'Authorization': `Bearer ${ACCESS_TOKEN}`,
'Content-Type': 'application/json'
}
});
return { success: true, messageId: response.data.messages[0].id };
} catch (error) {
return {
success: false,
error: error.response?.data?.error || error.message
};
}
}
// Broadcast ke banyak nomor
async function broadcastTemplate(recipients, templateName, templateParams) {
const results = [];
for (const recipient of recipients) {
const result = await sendTemplateMessage(
recipient.phone,
templateName,
templateParams
);
results.push({
phone: recipient.phone,
...result
});
// Rate limiting: 80/second = ~12ms delay
await sleep(15);
}
return results;
}Unofficial API Broadcast (Baileys):
javascript
const { default: makeWASocket } = require('@whiskeysockets/baileys');
async function broadcastMessage(sock, recipients, message) {
const results = [];
for (let i = 0; i < recipients.length; i++) {
const recipient = recipients[i];
const jid = `${recipient.phone}@s.whatsapp.net`;
try {
await sock.sendMessage(jid, { text: message });
results.push({
phone: recipient.phone,
success: true
});
console.log(`[${i+1}/${recipients.length}] Sent to ${recipient.phone}`);
} catch (error) {
results.push({
phone: recipient.phone,
success: false,
error: error.message
});
}
// PENTING: Delay untuk avoid ban!
// 3-5 detik antar pesan
await sleep(3000 + Math.random() * 2000);
// Jeda lebih lama setiap 50 pesan
if ((i + 1) % 50 === 0) {
console.log('Pausing for 5 minutes...');
await sleep(5 * 60 * 1000);
}
}
return results;
}Queue System
Bull Queue untuk Broadcast:
javascript
const Queue = require('bull');
const broadcastQueue = new Queue('whatsapp-broadcast', {
redis: { host: 'localhost', port: 6379 }
});
// Add job to queue
async function scheduleBroadcast(campaign) {
for (const recipient of campaign.recipients) {
await broadcastQueue.add({
campaignId: campaign.id,
phone: recipient.phone,
templateName: campaign.templateName,
templateParams: campaign.templateParams
}, {
delay: calculateDelay(recipient.index),
attempts: 3,
backoff: {
type: 'exponential',
delay: 60000
}
});
}
}
// Process queue
broadcastQueue.process(async (job) => {
const { phone, templateName, templateParams } = job.data;
const result = await sendTemplateMessage(phone, templateName, templateParams);
if (!result.success) {
throw new Error(result.error);
}
return result;
});
// Track progress
broadcastQueue.on('completed', async (job, result) => {
await updateCampaignProgress(job.data.campaignId, 'sent');
});
broadcastQueue.on('failed', async (job, error) => {
await updateCampaignProgress(job.data.campaignId, 'failed', error.message);
});Throttle Control:
javascript
const Bottleneck = require('bottleneck');
// Official API: 80/second = reservoir 80, minTime 12ms
const officialLimiter = new Bottleneck({
reservoir: 80,
reservoirRefreshAmount: 80,
reservoirRefreshInterval: 1000, // per second
minTime: 12
});
// Unofficial: 1 per 3 seconds, safer
const unofficialLimiter = new Bottleneck({
minTime: 3000,
maxConcurrent: 1
});
// Usage
async function sendWithLimit(phone, message) {
return await officialLimiter.schedule(() =>
sendTemplateMessage(phone, message)
);
}Personalisasi Broadcast
Template dengan Variables:
javascript
async function sendPersonalizedBroadcast(recipients) {
for (const recipient of recipients) {
// Personalize template params
const templateParams = [
{
type: 'body',
parameters: [
{ type: 'text', text: recipient.name },
{ type: 'text', text: recipient.lastProduct },
{ type: 'text', text: recipient.discountCode }
]
}
];
await sendTemplateMessage(
recipient.phone,
'promo_personal',
templateParams
);
await sleep(15);
}
}
// Template di Meta:
// "Hai {{1}}! Produk {{2}} favorit kamu lagi promo!
// Pakai kode {{3}} untuk diskon 20%!"Tracking & Analytics
Track Broadcast Campaign:
javascript
const campaign = {
id: 'CAMP-20260217-001',
name: 'Promo February',
templateName: 'promo_feb',
recipients: 1000,
status: 'running',
stats: {
queued: 1000,
sent: 0,
delivered: 0,
read: 0,
failed: 0
},
startedAt: new Date()
};
// Update dari webhook
async function handleStatusUpdate(status) {
const campaignId = await getCampaignFromMessageId(status.id);
if (status.status === 'sent') {
await db.campaigns.updateOne(
{ id: campaignId },
{ $inc: { 'stats.sent': 1, 'stats.queued': -1 } }
);
}
if (status.status === 'delivered') {
await db.campaigns.updateOne(
{ id: campaignId },
{ $inc: { 'stats.delivered': 1 } }
);
}
if (status.status === 'read') {
await db.campaigns.updateOne(
{ id: campaignId },
{ $inc: { 'stats.read': 1 } }
);
}
}
// Dashboard
function getCampaignStats(campaign) {
return {
deliveryRate: (campaign.stats.delivered / campaign.stats.sent * 100).toFixed(2) + '%',
readRate: (campaign.stats.read / campaign.stats.delivered * 100).toFixed(2) + '%',
failureRate: (campaign.stats.failed / campaign.recipients * 100).toFixed(2) + '%'
};
}Segmentation
javascript
// Segment recipients sebelum broadcast
async function segmentRecipients() {
return {
vip: await db.customers.find({ tier: 'vip', optIn: true }),
active: await db.customers.find({
lastOrder: { $gte: daysAgo(30) },
optIn: true
}),
dormant: await db.customers.find({
lastOrder: { $lt: daysAgo(90) },
optIn: true
}),
newCustomers: await db.customers.find({
createdAt: { $gte: daysAgo(7) },
optIn: true
})
};
}
// Different campaigns per segment
async function runSegmentedCampaign() {
const segments = await segmentRecipients();
// VIP: send first, best timing
await scheduleBroadcast({
recipients: segments.vip,
templateName: 'vip_exclusive',
scheduledAt: '10:00'
});
// Active: regular promo
await scheduleBroadcast({
recipients: segments.active,
templateName: 'promo_regular',
scheduledAt: '12:00'
});
// Dormant: win-back offer
await scheduleBroadcast({
recipients: segments.dormant,
templateName: 'winback_offer',
scheduledAt: '14:00'
});
}Best Practices
DO ✅
- Gunakan template (official)
- Respect rate limits
- Queue untuk volume besar
- Personalisasi konten
- Track delivery & read
- Opt-in only!
- Segment recipientsDON'T ❌
- Spam tanpa consent
- Ignore rate limits
- Kirim semua sekaligus
- Generic message ke semua
- Skip tracking
- Blast ke non-opt-in
- Same message ke everyoneFAQ
Berapa banyak bisa kirim per hari?
Official: Tergantung tier (1K - unlimited). Unofficial: Max 200-300 safely.
Perlu opt-in?
WAJIB! Tanpa opt-in = spam = ban + legal issues.
Kenapa message failed?
Cek: Invalid number, blocked, rate limited, template rejected.
Kesimpulan
Broadcast smart, not hard!
| Spam Broadcast | Smart Broadcast |
|---|---|
| Blast semua | Segmented |
| No personalization | Personalized |
| Ignore limits | Respect limits |
| No tracking | Full analytics |