Referensi lengkap REST API Waslah. Authentication, endpoints, error codes, rate limit per plan.
Paste token dari API Keys, semua snippet code di bawah akan otomatis terisi token-mu.
Waslah REST API memungkinkan integrasi WhatsApp dengan sistem apa pun — e-commerce, CRM, ERP, atau script internal. Cocok untuk notifikasi transaksional, OTP, reminder, dan auto reply.
Semua endpoint (kecuali webhook receiver) butuh API key. Kirim via header Authorization Bearer atau X-API-Key:
curl -H "Authorization: Bearer wsl_YOUR_TOKEN_HERE" \
https://waslah.id/api/v1/me
fetch('https://waslah.id/api/v1/me', {
headers: { 'Authorization': 'Bearer wsl_YOUR_TOKEN_HERE' }
})
.then(r => r.json())
.then(console.log);
<?php
$ch = curl_init('https://waslah.id/api/v1/me');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer wsl_YOUR_TOKEN_HERE',
'Accept: application/json',
]);
$response = curl_exec($ch);
echo $response;
import requests
r = requests.get(
'https://waslah.id/api/v1/me',
headers={'Authorization': 'Bearer wsl_YOUR_TOKEN_HERE'}
)
print(r.json())
REST API access (Pro ke atas). User Free akan dapat
403 plan_feature_required walau API key valid.
Beberapa feature spesifik juga gated:
scheduled_at (Basic+ → Schedule message).
Semua endpoint dimulai dengan base URL berikut:
https://waslah.id/api/v1
https://waslah.id/api/v1/meDapatkan info user pemilik API key + metadata key (request count, last used, scopes). Tidak butuh scope khusus.
{
"ok": true,
"data": {
"user": {
"id": 1,
"name": "Admin",
"email": "admin@waslah.id"
},
"api_key": {
"id": 2,
"name": "Production Server",
"prefix": "wsl_a3f29d4b",
"scopes": ["messages:send", "messages:read", "instances:read"],
"request_count": 42,
"last_used_at": "2026-05-11T17:24:02+00:00",
"expires_at": null
}
}
}
curl -H "Authorization: Bearer wsl_YOUR_TOKEN_HERE" \
https://waslah.id/api/v1/me
const res = await fetch('https://waslah.id/api/v1/me', {
headers: { 'Authorization': 'Bearer wsl_YOUR_TOKEN_HERE' }
});
const { data } = await res.json();
console.log(data.user.email);
$client = new \GuzzleHttp\Client();
$res = $client->get('https://waslah.id/api/v1/me', [
'headers' => ['Authorization' => 'Bearer wsl_YOUR_TOKEN_HERE'],
]);
$data = json_decode($res->getBody(), true);
import requests
r = requests.get(
'https://waslah.id/api/v1/me',
headers={'Authorization': 'Bearer wsl_YOUR_TOKEN_HERE'}
)
data = r.json()['data']
print(data['user']['email'])
https://waslah.id/api/v1/instancesList semua instance WhatsApp milik user.
{
"ok": true,
"data": [
{
"id": 1,
"key": "fathur-utama",
"name": "fathur romadhon",
"status": "connected",
"phone_number": "6285815004099",
"connected_at": "2026-05-11T16:32:49+00:00",
"last_seen_at": "2026-05-11T17:30:00+00:00",
"created_at": "2026-05-10T...",
"disconnected_at": null
}
]
}
curl -H "Authorization: Bearer wsl_YOUR_TOKEN_HERE" \
https://waslah.id/api/v1/instances
const res = await fetch('https://waslah.id/api/v1/instances', {
headers: { 'Authorization': 'Bearer wsl_YOUR_TOKEN_HERE' }
});
const { data } = await res.json();
data.forEach(i => console.log(i.key, i.status));
$res = (new \GuzzleHttp\Client())->get('https://waslah.id/api/v1/instances', [
'headers' => ['Authorization' => 'Bearer wsl_YOUR_TOKEN_HERE'],
]);
$instances = json_decode($res->getBody(), true)['data'];
r = requests.get(
'https://waslah.id/api/v1/instances',
headers={'Authorization': 'Bearer wsl_YOUR_TOKEN_HERE'}
)
for inst in r.json()['data']:
print(inst['key'], inst['status'])
https://waslah.id/api/v1/instances/{key}Detail satu instance berdasarkan key.
| Param | Tipe | Deskripsi |
|---|---|---|
key | string | Key instance, hanya huruf/angka/dash/underscore. |
curl -H "Authorization: Bearer wsl_YOUR_TOKEN_HERE" \
https://waslah.id/api/v1/instances/fathur-utama
const res = await fetch('https://waslah.id/api/v1/instances/fathur-utama', {
headers: { 'Authorization': 'Bearer wsl_YOUR_TOKEN_HERE' }
});
$res = (new \GuzzleHttp\Client())->get('https://waslah.id/api/v1/instances/fathur-utama', [
'headers' => ['Authorization' => 'Bearer wsl_YOUR_TOKEN_HERE'],
]);
r = requests.get(
'https://waslah.id/api/v1/instances/fathur-utama',
headers={'Authorization': 'Bearer wsl_YOUR_TOKEN_HERE'}
)
https://waslah.id/api/v1/messagesUnified endpoint — auto-detect tipe pesan berdasarkan body. Endpoint paling fleksibel untuk integrasi external app.
| Kalau body punya... | Routed ke |
|---|---|
cuma text | Send Text (lihat /messages/text) |
url atau media_url atau type | Send Media (lihat /messages/media) — default type=image kalau gak di-set |
| tidak ada text/url | 422 validation error |
Text message:
{
"instance_key": "fathur-utama",
"to": "6285815004099",
"text": "Halo dari API"
}
Media (image):
{
"instance_key": "fathur-utama",
"to": "6285815004099",
"url": "https://picsum.photos/600/400",
"caption": "Foto promo"
}
Document with explicit type:
{
"instance_key": "fathur-utama",
"to": "6285815004099",
"type": "document",
"url": "https://example.com/invoice.pdf",
"filename": "Invoice.pdf"
}
Field params, response shape, dan error codes identik dengan
/messages/text /
/messages/media tergantung yang ke-route.
to juga menerima JID utuh
(62812xxx@s.whatsapp.net, 12345@lid, 120363xxx@g.us).
Pakai ini untuk reply ke customer yang chat pakai mode privasi 2024+ (LID) — kirim ke phone biasa tidak akan sampai.
Ambil remote_jid dari webhook message.received lalu pakai persis di field to.
{
"instance_key": "fathur-utama",
"to": "123456789012345@lid",
"text": "Halo, terima kasih sudah menghubungi kami."
}
curl -X POST https://waslah.id/api/v1/messages \
-H "Authorization: Bearer wsl_YOUR_TOKEN_HERE" \
-H "Content-Type: application/json" \
-d '{
"instance_key": "fathur-utama",
"to": "6285815004099",
"text": "Halo dari API"
}'
// Text
await fetch('https://waslah.id/api/v1/messages', {
method: 'POST',
headers: {
'Authorization': 'Bearer wsl_YOUR_TOKEN_HERE',
'Content-Type': 'application/json',
},
body: JSON.stringify({
instance_key: 'fathur-utama',
to: '6285815004099',
text: 'Halo',
}),
});
// Atau dengan media — sistem auto-detect dari url
await fetch('https://waslah.id/api/v1/messages', {
method: 'POST',
headers: { /* sama */ },
body: JSON.stringify({
instance_key: 'fathur-utama',
to: '6285815004099',
url: 'https://picsum.photos/600/400',
caption: 'Foto promo',
}),
});
$client = new \GuzzleHttp\Client();
// Text
$client->post('https://waslah.id/api/v1/messages', [
'headers' => ['Authorization' => 'Bearer wsl_YOUR_TOKEN_HERE'],
'json' => [
'instance_key' => 'fathur-utama',
'to' => '6285815004099',
'text' => 'Halo',
],
]);
// Media — auto-detect karena ada url
$client->post('https://waslah.id/api/v1/messages', [
'headers' => ['Authorization' => 'Bearer wsl_YOUR_TOKEN_HERE'],
'json' => [
'instance_key' => 'fathur-utama',
'to' => '6285815004099',
'url' => 'https://picsum.photos/600/400',
],
]);
import requests
h = {'Authorization': 'Bearer wsl_YOUR_TOKEN_HERE'}
# Text
requests.post('https://waslah.id/api/v1/messages', headers=h, json={
'instance_key': 'fathur-utama',
'to': '6285815004099',
'text': 'Halo',
})
# Media — auto-detect
requests.post('https://waslah.id/api/v1/messages', headers=h, json={
'instance_key': 'fathur-utama',
'to': '6285815004099',
'url': 'https://picsum.photos/600/400',
})
https://waslah.id/api/v1/messages/textKirim pesan teks explicit (text-only endpoint). Endpoint return 202 Accepted langsung; kerjaan kirim ke WhatsApp dijalankan via queue. Cek status final di GET /messages/{id}.
POST /messages — auto-detect text vs media.
| Field | Tipe | Wajib | Keterangan |
|---|---|---|---|
instance_key | string | ✓ | Key instance yang sudah connected. |
to | string | ✓ | Nomor tujuan (62..., 08..., +62...) — auto dinormalisasi. Atau JID utuh untuk reply ke conversation existing: 62812xxx@s.whatsapp.net, 12345@lid (WhatsApp privacy), 120363xxx@g.us (group). Pakai remote_jid dari webhook message.received persis untuk reply LID — kirim ke phone biasa tidak akan sampai ke LID user. |
text | string | ✓ | Isi pesan (max 4000 karakter). |
scheduled_at | ISO 8601 datetime | — | Jadwalkan pesan untuk dikirim di masa depan (min 30 detik dari sekarang). Butuh plan Basic+ (feature Schedule message). Format: 2026-05-21T09:00:00+07:00. |
{
"instance_key": "fathur-utama",
"to": "6285815004099",
"text": "Halo, ini notifikasi dari sistem kami.",
"scheduled_at": "2026-05-21T09:00:00+07:00"
}
{
"ok": true,
"data": {
"id": 5,
"status": "pending",
"instance_key": "fathur-utama",
"to": "6285815004099",
"preview": "Halo, ini notifikasi...",
"scheduled_at": "2026-05-21T09:00:00+07:00",
"check_status_url": "https://waslah.id/api/v1/messages/5",
"created_at": "2026-05-19T..."
}
}
{
"ok": false,
"error": "instance_not_connected",
"message": "Instance `xxx` sedang `disconnected`. Hanya bisa kirim saat status `connected`.",
"instance_status": "disconnected"
}
{
"ok": false,
"error": "plan_feature_required",
"message": "Schedule message butuh plan Basic atau lebih tinggi.",
"required_feature": "Schedule message",
"active_plan": "Free"
}
curl -X POST https://waslah.id/api/v1/messages/text \
-H "Authorization: Bearer wsl_YOUR_TOKEN_HERE" \
-H "Content-Type: application/json" \
-d '{
"instance_key": "fathur-utama",
"to": "6285815004099",
"text": "Halo dari API"
}'
const res = await fetch('https://waslah.id/api/v1/messages/text', {
method: 'POST',
headers: {
'Authorization': 'Bearer wsl_YOUR_TOKEN_HERE',
'Content-Type': 'application/json',
},
body: JSON.stringify({
instance_key: 'fathur-utama',
to: '6285815004099',
text: 'Halo dari API',
}),
});
const { data } = await res.json();
console.log('Queued:', data.id);
$client = new \GuzzleHttp\Client();
$res = $client->post('https://waslah.id/api/v1/messages/text', [
'headers' => [
'Authorization' => 'Bearer wsl_YOUR_TOKEN_HERE',
'Accept' => 'application/json',
],
'json' => [
'instance_key' => 'fathur-utama',
'to' => '6285815004099',
'text' => 'Halo dari API',
],
]);
$message = json_decode($res->getBody(), true)['data'];
import requests
r = requests.post(
'https://waslah.id/api/v1/messages/text',
headers={
'Authorization': 'Bearer wsl_YOUR_TOKEN_HERE',
'Content-Type': 'application/json',
},
json={
'instance_key': 'fathur-utama',
'to': '6285815004099',
'text': 'Halo dari API',
}
)
print(r.json()['data']['id'])
https://waslah.id/api/v1/messages/mediaKirim gambar, dokumen, video, atau audio via URL. Engine yang fetch URL + upload ke WhatsApp.
| Field | Tipe | Wajib | Keterangan |
|---|---|---|---|
instance_key | string | ✓ | Key instance connected. |
to | string | ✓ | Nomor tujuan, ATAU JID utuh (...@s.whatsapp.net, ...@lid, ...@g.us) untuk reply ke conversation LID/group — sama seperti /messages/text. |
type | enum | ✓ | image, document, video, audio. |
url | url | ✓ | URL public file media (max 2048 char). Tidak boleh localhost / 127.0.0.1 / private IP — engine akan reject. Pakai POST /uploads kalau gak punya host public. |
caption | string | — | Teks di bawah media (max 1024 char). |
filename | string | — | Nama file untuk type document. |
mimetype | string | — | MIME type override. |
scheduled_at | ISO 8601 | — | Jadwalkan untuk dikirim nanti (min 30 detik dari sekarang). Butuh plan Basic+. |
{
"instance_key": "fathur-utama",
"to": "6285815004099",
"type": "image",
"url": "https://picsum.photos/600/400",
"caption": "Foto promo bulan ini"
}
{
"ok": true,
"data": {
"id": 12,
"status": "pending",
"instance_key": "fathur-utama",
"to": "6285815004099",
"type": "image",
"media_url": "https://picsum.photos/600/400",
"caption": "Foto promo bulan ini",
"scheduled_at": null,
"check_status_url": "https://waslah.id/api/v1/messages/12",
"created_at": "2026-05-19T..."
}
}
curl -X POST https://waslah.id/api/v1/messages/media \
-H "Authorization: Bearer wsl_YOUR_TOKEN_HERE" \
-H "Content-Type: application/json" \
-d '{
"instance_key": "fathur-utama",
"to": "6285815004099",
"type": "image",
"url": "https://picsum.photos/600/400",
"caption": "Foto promo"
}'
await fetch('https://waslah.id/api/v1/messages/media', {
method: 'POST',
headers: {
'Authorization': 'Bearer wsl_YOUR_TOKEN_HERE',
'Content-Type': 'application/json',
},
body: JSON.stringify({
instance_key: 'fathur-utama',
to: '6285815004099',
type: 'image',
url: 'https://picsum.photos/600/400',
caption: 'Foto promo',
}),
});
$client = new \GuzzleHttp\Client();
$client->post('https://waslah.id/api/v1/messages/media', [
'headers' => ['Authorization' => 'Bearer wsl_YOUR_TOKEN_HERE'],
'json' => [
'instance_key' => 'fathur-utama',
'to' => '6285815004099',
'type' => 'image',
'url' => 'https://picsum.photos/600/400',
'caption' => 'Foto promo',
],
]);
requests.post(
'https://waslah.id/api/v1/messages/media',
headers={'Authorization': 'Bearer wsl_YOUR_TOKEN_HERE'},
json={
'instance_key': 'fathur-utama',
'to': '6285815004099',
'type': 'image',
'url': 'https://picsum.photos/600/400',
'caption': 'Foto promo',
}
)
https://waslah.id/api/v1/messages/bulkBroadcast pesan ke max 100 nomor sekaligus. Buat 1 broadcast record + N pesan, dispatch ke queue paralel. Cek progress via GET /broadcasts/{id}.
| Field | Tipe | Wajib | Keterangan |
|---|---|---|---|
instance_key | string | ✓ | Key instance connected. |
recipients | string[] | ✓ | Array nomor tujuan (max 100). Duplikat auto-deduped. |
name | string | — | Nama broadcast untuk identifikasi (max 200 char). |
type | enum | ✓ | text, image, document, video, audio. |
text | string | ✓¹ | Wajib kalau type=text. |
url | url | ✓² | Wajib kalau type ≠ text. |
caption, filename, mimetype | string | — | Untuk media (lihat /messages/media). |
delay_min_sec | int | — | Jeda minimum antar pesan (0–300 detik). Default 3. |
delay_max_sec | int | — | Jeda maksimum. Sama dengan min = fixed. Beda = random range. Default 8. |
delay_min_sec=3, delay_max_sec=8 untuk jeda random natural.
{
"ok": true,
"data": {
"id": 3,
"name": "Promo Akhir Bulan",
"message_type": "text",
"instance_key": "fathur-utama",
"status": "processing",
"total": 50,
"sent": 0,
"failed": 0,
"pending": 50,
"progress_percent": 0,
"delay_min_sec": 3,
"delay_max_sec": 8,
"delay_label": "3–8 detik",
"created_at": "2026-05-19T...",
"completed_at": null,
"check_status_url": "https://waslah.id/api/v1/broadcasts/3"
}
}
{
"ok": false,
"error": "no_valid_recipients",
"message": "Tidak ada nomor valid (minimal 8 digit, max 15 digit)."
}
curl -X POST https://waslah.id/api/v1/messages/bulk \
-H "Authorization: Bearer wsl_YOUR_TOKEN_HERE" \
-H "Content-Type: application/json" \
-d '{
"instance_key": "fathur-utama",
"name": "Promo Akhir Bulan",
"type": "text",
"text": "Halo, ada promo nih!",
"recipients": ["62812xxx", "62813xxx", "62814xxx"]
}'
await fetch('https://waslah.id/api/v1/messages/bulk', {
method: 'POST',
headers: {
'Authorization': 'Bearer wsl_YOUR_TOKEN_HERE',
'Content-Type': 'application/json',
},
body: JSON.stringify({
instance_key: 'fathur-utama',
name: 'Promo Akhir Bulan',
type: 'text',
text: 'Halo, ada promo nih!',
recipients: ['62812xxx', '62813xxx', '62814xxx'],
}),
});
$client = new \GuzzleHttp\Client();
$res = $client->post('https://waslah.id/api/v1/messages/bulk', [
'headers' => ['Authorization' => 'Bearer wsl_YOUR_TOKEN_HERE'],
'json' => [
'instance_key' => 'fathur-utama',
'name' => 'Promo Akhir Bulan',
'type' => 'text',
'text' => 'Halo, ada promo nih!',
'recipients' => ['62812xxx', '62813xxx', '62814xxx'],
],
]);
$broadcastId = json_decode($res->getBody(), true)['data']['id'];
r = requests.post(
'https://waslah.id/api/v1/messages/bulk',
headers={'Authorization': 'Bearer wsl_YOUR_TOKEN_HERE'},
json={
'instance_key': 'fathur-utama',
'name': 'Promo Akhir Bulan',
'type': 'text',
'text': 'Halo, ada promo nih!',
'recipients': ['62812xxx', '62813xxx', '62814xxx'],
}
)
broadcast_id = r.json()['data']['id']
https://waslah.id/api/v1/uploadsUpload file (multipart/form-data). Return public URL yang bisa langsung dipakai untuk /messages/media atau /messages/bulk. Max 16MB. Tidak konsumsi quota.
Image: jpeg, png, webp, gif · Video: mp4, quicktime · Audio: mpeg, mp4, ogg, wav, aac · Document: pdf, doc/docx, xls/xlsx, ppt/pptx, txt, csv, zip
{
"ok": true,
"data": {
"url": "https://waslah.id/storage/uploads/2026-05/9c5b...-uuid.jpg",
"path": "uploads/2026-05/9c5b...-uuid.jpg",
"filename": "kucing.jpg",
"size": 245870,
"mime_type": "image/jpeg",
"type": "image"
}
}
url = public URL siap pakai (paste ke /messages/media.url).
path = relative storage path (untuk delete/admin use). type = auto-detect dari MIME (image|video|audio|document).
{
"ok": false,
"error": "unsupported_media_type",
"message": "MIME type `application/x-foo` tidak didukung. Cek dokumentasi untuk format yang diterima.",
"mime_type": "application/x-foo"
}
{
"ok": false,
"error": "validation_failed",
"message": "File tidak valid: The file field is required.",
"errors": {
"file": ["The file field is required."]
}
}
curl -X POST https://waslah.id/api/v1/uploads \
-H "Authorization: Bearer wsl_YOUR_TOKEN_HERE" \
-F "file=@/path/to/image.jpg"
const fd = new FormData();
fd.append('file', fileInput.files[0]);
const res = await fetch('https://waslah.id/api/v1/uploads', {
method: 'POST',
headers: { 'Authorization': 'Bearer wsl_YOUR_TOKEN_HERE' },
body: fd,
});
const { data } = await res.json();
console.log('Uploaded URL:', data.url);
$client = new \GuzzleHttp\Client();
$res = $client->post('https://waslah.id/api/v1/uploads', [
'headers' => ['Authorization' => 'Bearer wsl_YOUR_TOKEN_HERE'],
'multipart' => [
['name' => 'file', 'contents' => fopen('/path/to/image.jpg', 'r')],
],
]);
$url = json_decode($res->getBody(), true)['data']['url'];
with open('/path/to/image.jpg', 'rb') as f:
r = requests.post(
'https://waslah.id/api/v1/uploads',
headers={'Authorization': 'Bearer wsl_YOUR_TOKEN_HERE'},
files={'file': f},
)
url = r.json()['data']['url']
https://waslah.id/api/v1/messages/{id}Cek status pesan. Status bisa pending, sent, delivered, read, failed.
{
"ok": true,
"data": {
"id": 5,
"instance_key": "fathur-utama",
"direction": "outbound",
"status": "sent",
"message_type": "text",
"phone_number": "6285815004099",
"remote_jid": "6285815004099@s.whatsapp.net",
"body": "Halo dari API",
"media_url": null,
"media_filename": null,
"media_mime_type": null,
"provider_message_id": "BAE5...",
"error_message": null,
"sent_at": "2026-05-19T...",
"received_at": null,
"scheduled_at": null,
"created_at": "2026-05-19T...",
"updated_at": "2026-05-19T..."
}
}
Untuk pesan media: message_type = image|video|audio|document|sticker,
media_url berisi URL file. Untuk reply ke conversation LID (privacy mode),
remote_jid akan berakhiran @lid alih-alih @s.whatsapp.net —
pakai value remote_jid persis di field to saat call
/messages/text /
/messages/media untuk reply ke customer LID.
{
"ok": false,
"error": "not_found",
"message": "Message dengan ID `99999` tidak ditemukan atau bukan milik Anda."
}
curl -H "Authorization: Bearer wsl_YOUR_TOKEN_HERE" \
https://waslah.id/api/v1/messages/5
// Poll status sampai final (sent/delivered/failed)
async function pollStatus(id, token) {
while (true) {
const r = await fetch(`https://waslah.id/api/v1/messages/${id}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const { data } = await r.json();
if (data.status !== 'pending') return data;
await new Promise(r => setTimeout(r, 2000));
}
}
function pollStatus(int $id, string $token): array {
$client = new \GuzzleHttp\Client();
while (true) {
$res = $client->get("https://waslah.id/api/v1/messages/$id", [
'headers' => ['Authorization' => "Bearer $token"]
]);
$data = json_decode($res->getBody(), true)['data'];
if ($data['status'] !== 'pending') return $data;
sleep(2);
}
}
import requests, time
def poll_status(id, token):
while True:
r = requests.get(
f'https://waslah.id/api/v1/messages/{id}',
headers={'Authorization': f'Bearer {token}'}
)
data = r.json()['data']
if data['status'] != 'pending':
return data
time.sleep(2)
https://waslah.id/api/v1/broadcastsList 50 broadcast terakhir milik user dengan progress real-time.
curl -H "Authorization: Bearer wsl_YOUR_TOKEN_HERE" \
https://waslah.id/api/v1/broadcasts
const res = await fetch('https://waslah.id/api/v1/broadcasts', {
headers: { 'Authorization': 'Bearer wsl_YOUR_TOKEN_HERE' }
});
const { data } = await res.json();
data.forEach(b => console.log(b.id, b.status, `${b.sent}/${b.total}`));
$res = (new \GuzzleHttp\Client())->get('https://waslah.id/api/v1/broadcasts', [
'headers' => ['Authorization' => 'Bearer wsl_YOUR_TOKEN_HERE'],
]);
r = requests.get('https://waslah.id/api/v1/broadcasts',
headers={'Authorization': 'Bearer wsl_YOUR_TOKEN_HERE'}
)
for b in r.json()['data']:
print(b['id'], b['status'], f"{b['sent']}/{b['total']}")
https://waslah.id/api/v1/broadcasts/{id}Cek progress real-time broadcast. Poll endpoint ini sampai status jadi completed atau failed.
{
"ok": true,
"data": {
"id": 3,
"name": "Promo Akhir Bulan",
"message_type": "text",
"instance_key": "fathur-utama",
"status": "processing",
"total": 50,
"sent": 32,
"failed": 1,
"pending": 17,
"progress_percent": 66,
"delay_min_sec": 3,
"delay_max_sec": 8,
"delay_label": "3–8 detik",
"created_at": "2026-05-19T...",
"completed_at": null
}
}
status ∈ queued|processing|completed|failed.
delay_label human-readable (mis. 3 detik (fixed) kalau min=max).
completed_at diisi saat status final.
curl -H "Authorization: Bearer wsl_YOUR_TOKEN_HERE" \
https://waslah.id/api/v1/broadcasts/3
// Poll progress sampai selesai
async function trackBroadcast(id, token) {
while (true) {
const r = await fetch(`https://waslah.id/api/v1/broadcasts/${id}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const { data } = await r.json();
console.log(`${data.progress_percent}% — sent ${data.sent}/${data.total}`);
if (data.status !== 'processing') return data;
await new Promise(r => setTimeout(r, 3000));
}
}
function trackBroadcast(int $id, string $token): array {
$client = new \GuzzleHttp\Client();
while (true) {
$res = $client->get("https://waslah.id/api/v1/broadcasts/$id", [
'headers' => ['Authorization' => "Bearer $token"]
]);
$data = json_decode($res->getBody(), true)['data'];
echo $data['progress_percent'] . "% — sent {$data['sent']}/{$data['total']}\n";
if ($data['status'] !== 'processing') return $data;
sleep(3);
}
}
import time
def track_broadcast(id, token):
while True:
r = requests.get(f'https://waslah.id/api/v1/broadcasts/{id}',
headers={'Authorization': f'Bearer {token}'}
)
d = r.json()['data']
print(f"{d['progress_percent']}% — sent {d['sent']}/{d['total']}")
if d['status'] != 'processing':
return d
time.sleep(3)
Semua error response punya format konsisten:
{
"ok": false,
"error": "error_code_string",
"message": "Human-readable message",
...additional context...
}
| HTTP | Error Code | Penyebab |
|---|---|---|
| 401 | unauthorized | Token tidak ada, salah, atau di-revoke. |
| 403 | forbidden_scope | API key tidak punya scope yang dibutuhkan endpoint. |
| 403 | plan_feature_required | Plan kamu belum punya fitur yang dibutuhkan (mis. Schedule message, REST API access). Response include required_feature + active_plan. |
| 404 | not_found / instance_not_found | Resource tidak ada atau bukan milik kamu. |
| 409 | instance_not_connected | Mencoba kirim pesan saat instance tidak `connected`. |
| 422 | validation_failed | Body request tidak lolos validasi. Cek field errors. |
| 422 | no_valid_recipients | Broadcast: tidak ada nomor valid setelah normalize (min 8 digit, max 15). |
| 422 | unsupported_media_type | Upload: MIME type tidak ada di whitelist. |
| 429 | — | Rate limit terlampaui. Tunggu sampai X-RateLimit-Reset. |
| 500 | — | Server error. Lapor ke support kalau berulang. |
| 502 | — | Engine down / tidak respon. Coba lagi. |
Limit di-scope per API key, di-track per menit. Kalau exceed, response 429 Too Many Requests.
| Plan | Request / menit |
|---|---|
| Free | 60 |
| Basic | 300 |
| Pro | 1,000 |
| Enterprise | 5,000 |
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 47
X-RateLimit-Reset: 1699999999
Retry-After: 12
POST /messages/bulk supaya hemat quota request.
Scope membatasi apa yang bisa dilakukan API key. Set seminimal mungkin (principle of least privilege).
| Scope | Akses |
|---|---|
messages:send |
Kirim pesan |
messages:read |
Baca histori pesan |
instances:read |
Lihat status instance |
instances:write |
Kelola instance (start/disconnect) |
Daftarkan URL endpoint kamu — Waslah akan POST event real-time saat ada pesan masuk, status update, dst. Butuh plan Basic+.
secret yang ter-generate — dipakai untuk verify signature| Event | Trigger |
|---|---|
message.received | Pesan masuk dari customer |
message.sent | Pesan outbound berhasil dikirim ke WA (status: queued) |
message.status | Update status: delivered, read, played |
Setiap webhook POST mengirim headers berikut. Signature dikirim di 5 alias header sekaligus untuk maximum compatibility dengan berbagai framework — pilih salah satu di kode validator kamu.
| Header | Contoh Value | Catatan |
|---|---|---|
X-Waslah-Signature | sha256=<hex> | Primary — recommended |
X-Signature | sha256=<hex> | Alias generic |
Signature | sha256=<hex> | Alias bare |
X-Hub-Signature-256 | sha256=<hex> | GitHub convention |
X-Webhook-Signature | sha256=<hex> | Common convention |
X-Waslah-Timestamp | 1748036400 | Unix timestamp — dipakai di HMAC payload |
X-Waslah-Event | message.received | Event type |
X-Waslah-Event-Id | UUID | Unique per delivery (anti duplicate processing) |
User-Agent | Waslah-Webhook/1.0 | Identifier |
payload_to_sign = X-Waslah-Timestamp + "." + raw_request_body
signature = HMAC-SHA256(payload_to_sign, webhook_secret)
header_value = "sha256=" + bin2hex(signature)
$timestamp = $_SERVER['HTTP_X_WASLAH_TIMESTAMP'] ?? '';
$header = $_SERVER['HTTP_X_WASLAH_SIGNATURE'] ?? '';
$body = file_get_contents('php://input');
$secret = 'YOUR_WEBHOOK_SECRET'; // dari dashboard
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, $secret);
if (! hash_equals($expected, $header)) {
http_response_code(401);
exit(json_encode(['ok' => false, 'error' => 'Invalid signature']));
}
// Anti-replay: reject kalau timestamp > 5 menit lalu
if (abs(time() - (int) $timestamp) > 300) {
http_response_code(401);
exit(json_encode(['ok' => false, 'error' => 'Timestamp too old']));
}
$data = json_decode($body, true);
// ... process event ...
http_response_code(200);
echo json_encode(['ok' => true]);
const crypto = require('crypto');
const express = require('express');
const app = express();
// PENTING: raw body required untuk signature verify
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const timestamp = req.header('X-Waslah-Timestamp');
const received = req.header('X-Waslah-Signature');
const body = req.body.toString('utf8');
const secret = process.env.WASLAH_WEBHOOK_SECRET;
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(timestamp + '.' + body)
.digest('hex');
if (! crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))) {
return res.status(401).json({ ok: false, error: 'Invalid signature' });
}
if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) {
return res.status(401).json({ ok: false, error: 'Timestamp too old' });
}
const data = JSON.parse(body);
console.log('Event:', data.event, data.data);
res.json({ ok: true });
});
import hmac, hashlib, time
from flask import Flask, request, jsonify
app = Flask(__name__)
SECRET = b'YOUR_WEBHOOK_SECRET'
@app.route('/webhook', methods=['POST'])
def webhook():
timestamp = request.headers.get('X-Waslah-Timestamp', '')
received = request.headers.get('X-Waslah-Signature', '')
body = request.get_data()
expected = 'sha256=' + hmac.new(
SECRET, (timestamp + '.').encode() + body, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, received):
return jsonify(ok=False, error='Invalid signature'), 401
if abs(time.time() - int(timestamp)) > 300:
return jsonify(ok=False, error='Timestamp too old'), 401
data = request.get_json()
print('Event:', data['event'], data['data'])
return jsonify(ok=True)
hash_equals / timingSafeEqual / compare_digest — bukan === atau ==. Anti timing attack yang bisa leak signature byte-by-byte.
{
"event": "message.received",
"timestamp": 1715000000,
"data": {
"message_id": 1234,
"instance_key": "fathur-utama",
"from": "6281234567890",
"remote_jid": "6281234567890@s.whatsapp.net",
"body": "Halo, info produk",
"message_type": "text",
"received_at": "2026-05-12T11:05:23+07:00"
}
}
data.remote_jid dari payload webhook lalu pakai persis sebagai field to saat call POST /messages/text.
Untuk customer dengan WhatsApp privacy mode 2024+ (LID), remote_jid berakhiran @lid — harus pakai full JID, karena reply ke from (digit phone) tidak sampai ke nomor LID.
// Contoh auto-reply Node.js
app.post('/webhook', (req, res) => {
const { event, data } = req.body;
if (event === 'message.received') {
fetch('https://waslah.id/api/v1/messages/text', {
method: 'POST',
headers: { 'Authorization': 'Bearer wsl_xxx', 'Content-Type': 'application/json' },
body: JSON.stringify({
instance_key: data.instance_key,
to: data.remote_jid, // ← pakai JID utuh, bukan data.from
text: 'Terima kasih, pesan kamu sudah kami terima.'
})
});
}
res.json({ ok: true });
});