Domain produksi: https://crm.jemberkamera.id · Dev: · Auth: x-api-key: wg_xxx atau Authorization: Bearer <JWT> · Login · Dashboard · Health
Semua contoh pakai https://crm.jemberkamera.id — lokal ganti http://localhost:3000. Setiap kirim ada typing composing → paused + delay 3 detik (WIB = GMT+7 untuk jadwal).
BASE_URL=https://crm.jemberkamera.id PORT=3000 NODE_ENV=production TZ=Asia/Jakarta TZ_OFFSET_MINUTES=420 SEND_DELAY_MS=3000 SEND_TYPING_MS=2500 SCHEDULE_POLL_MS=15000 SCHEDULE_BATCH=20 WEBHOOK_RETRY_MAX=3 WEBHOOK_TIMEOUT_MS=5000 DB_HOST=... DB_PORT=3306 DB_USER=... DB_PASSWORD=... DB_NAME=whatsapp_gate MASTER_API_KEY=wg_xxx JWT_SECRET=..._min_32_chars ADMIN_USER=admin ADMIN_PASS=... ADMIN_EMAIL=admin@jemberkamera.id
Reverse proxy (Nginx/Caddy) → localhost:3000, HTTPS, cookie Secure di prod. Delay & typing bisa dioverride per-request delay_ms / typing_ms / typing:false.
.env.example → .env, isi BASE_URL=https://crm.jemberkamera.id, DB_*, MASTER_API_KEY, JWT_SECRET, TZ=Asia/Jakarta.npm install lalu npm run migrate — buat is_read/read_at + scheduled_messages. Atau mysql -u root -p < schema.sql.npm start (prod) / npm run dev. Cek /health → db: connected./login.html → /dashboard.html — pilih session dari dropdown (tanpa ketik manual). Atau API: POST /api/sessions { session_id:"cs-01" } → GET /api/sessions/cs-01/qr (scan WA).POST /api/messages/send-text { session_id, to, text } — otomatis delay 3s + typing. Jadwal: POST /api/schedules { session_id, to, text, send_at:"2026-08-29 21:00:00" } (WIB).Format nomor: 628123456789 atau JID 628123456789@s.whatsapp.net. Nol depan jadi 62. Group: xxx@g.us.
https://crm.jemberkamera.id· prod wajib HTTPS
Panel login balikan token (JWT) — pakai Authorization: Bearer <JWT> (juga cookie token / ?token=). Integrasi luar pakai x-api-key: wg_xxx atau Authorization: Bearer wg_xxx. Middleware /api/* terima keduanya.
| Method | Path | Body | Respon |
|---|---|---|---|
POST /api/auth/login | public | { username|email, password } | { success, token, user:{id,name,email,username,role} } + Set-Cookie token |
POST /api/auth/logout | - | - | clear cookie |
GET /api/auth/me | JWT | - | { user } |
POST https://crm.jemberkamera.id/api/auth/login
Content-Type: application/json
{ "username":"admin", "password":"admin123" }
// → { token:"eyJ...", user:{ username:"admin", email:"admin@jemberkamera.id" } }
GET https://crm.jemberkamera.id/api/auth/me
Authorization: Bearer <JWT>
| Method | Path | Auth | Deskripsi |
|---|---|---|---|
| GET | / | - | { name,version,docs,health,login,api } (HTML → redirect /login.html) |
| GET | /health | - | { db: "connected"|"not ready", sessions:[{session_id,status,jid,has_qr}], uptime } |
| GET | /docs | - | halaman ini |
| GET | /dashboard.html | JWT | Dashboard modern (select session, kirim, jadwal WIB, keys) |
| GET | /login.html | - | Login panel |
Dashboard pakai dropdown session dari GET /api/sessions — tidak perlu ketik manual. Session aktif tersimpan dan sync ke form kirim & jadwal.
| Method | Path |
|---|---|
POST /api/sessions | Body: { session_id:"cs-01", name:"Customer Service", webhook_url:"https://crm.jemberkamera.id/hook", webhook_enabled:true } → 201 { session_id, status:"connecting"|"qr" }. session_id dinormalkan [a-z0-9_-]. |
GET /api/sessions | List gabungan DB + live. { session_id, name, phone_number, jid, status, live_status, has_qr, webhook_url, webhook_enabled } |
GET /api/sessions/:id | Detail + live_status, is_live, jid_live |
GET /api/sessions/:id/qr | { status:"qr", qr:"...", qr_image:"data:image/png;base64,..." } atau status:"connected" |
GET /api/sessions/:id/status | { session_id, status, jid, phone } |
POST /api/sessions/:id/webhook | Body: { webhook_url, webhook_enabled:bool, webhook_events:["message.upsert","connection.update"] } |
POST /api/sessions/:id/restart | Restart socket |
DELETE /api/sessions/:id | Logout + hapus file auth + row DB |
POST https://crm.jemberkamera.id/api/sessions
x-api-key: wg_xxx
{ "session_id":"cs-01", "name":"CS Jember" }
GET https://crm.jemberkamera.id/api/sessions/cs-01/qr
// → { "data": { "qr_image": "data:image/png;base64,..." } } // tampilkan <img src=qr_image>
Human-like: tiap send otomatis typing composing → paused ke JID tujuan (≈ SEND_TYPING_MS=2500ms) lalu delay 3 detik (SEND_DELAY_MS=3000) sebelum sendMessage. ENV BASE_URL=https://crm.jemberkamera.id, TZ=Asia/Jakarta.
POST /api/messages/send-text
POST https://crm.jemberkamera.id/api/messages/send-text
x-api-key: wg_xxx
Content-Type: application/json
{
"session_id": "cs-01",
"to": "628123456789",
"text": "Halo dari JemberKamera!",
"quoted_message_id": "optional-WA_ID",
"typing": true,
"typing_ms": 2500,
"delay_ms": 3000
}
// → { success:true, data:{ message_id, to:"628123456789@s.whatsapp.net", to_number:"628123456789" } }
Override: typing:false = tanpa typing, delay_ms:0 = tanpa delay.
POST /api/messages/send-media — multipart file atau JSON URL (typing + delay sama, auto-detect tipe jika kosong)
POST https://crm.jemberkamera.id/api/messages/send-media
x-api-key: wg_xxx
# A) multipart
Content-Type: multipart/form-data
fields: session_id, to, caption, type=image|video|audio|document, file=@foto.jpg, typing, typing_ms, delay_ms
# B) JSON via URL
Content-Type: application/json
{ "session_id":"cs-01","to":"628123456789","type":"image","media_url":"https://crm.jemberkamera.id/file.jpg","caption":"Lihat ini","typing":true,"delay_ms":3000 }
File ≤25MB (uploads/). Jika type kosong, tebak dari mimetype/ekstensi.
POST /api/messages/send-bulk — max 50, tiap item typing + delay 3s + jeda 600ms
{
"session_id": "cs-01",
"typing": true,
"delay_ms": 3000,
"messages": [
{ "to":"628111","text":"Halo 1","typing_ms":2000,"delay_ms":3000 },
{ "to":"628222","text":"Halo 2" }
]
}
// → { success:true, data:[{to, success, message_id|error}] }
GET /api/messages?session_id=cs-01&direction=in|out&status=sent|delivered|read|failed|received|pending&is_read=0|1&limit=20&offset=0 — history DB. is_read butuh migrate (fallback otomatis).
GET /api/messages/:id — detail satu row
Semua chat masuk disimpan ke messages dengan direction="in", status="received", is_read=0 (unread). Chat keluar is_read=1. Ditambah read_at.
| Method | Path | Deskripsi |
|---|---|---|
GET /api/messages/unread-count?session_id=cs-01 | { total, by_session:[{session_id,cnt}] } | Hitung unread |
POST /api/messages/mark-read | Body salah satu: { session_id, message_ids:[WA_ID], ids:[dbId], jid:"628xxx@s.whatsapp.net", all:true } → { affected } | Set is_read=1, read_at=NOW() + sendReadReceipt jika jid |
PATCH /api/messages/:id/read | Satu DB id jadi read + receipt | |
GET /api/messages?is_read=0&direction=in | List unread saja |
EVENT messages.upsert → auto sendReadReceipt untuk non-group; status update via messages.update (2=delivered,3=read,4=played) + webhook message.status.
POST /api/messages/typing — typing manual ke lawan tanpa kirim
{ "session_id":"cs-01","to":"628123456789","duration_ms":2500 }
// → { success:true, message:"typing sent" } // composing detik lalu paused
Semua send sudah include typing otomatis; endpoint ini untuk efek ketik standalone.
Panel: Jadwal (GMT+7) di /dashboard.html pakai dropdown session + datetime-local WIB. API timezone fix Asia/Jakarta. Kolom send_at_wib (display) + send_at_utc (eksekusi UTC) di scheduled_messages. Worker poll SCHEDULE_POLL_MS=15000ms, kirim dengan typing + delay 3s.
| Method | Path | Ket |
|---|---|---|
POST /api/schedules | WIB: send_at (atau send_at_wib/scheduled_at) format "YYYY-MM-DD HH:mm:ss" atau "YYYY-MM-DDTHH:mm:ss+07:00" | Body: { session_id, to:"628xxx", type:"text|image|...", text/content, media_url, media_mimetype, send_at:"2026-08-29 21:00:00" } — validasi masa depan; butuh session ada |
GET /api/schedules?session_id=cs-01&status=pending|sent|failed|cancelled&limit=20&offset=0 | { timezone:"Asia/Jakarta (GMT+7)", data:[{id,session_id,to_jid,type,content,media_url,send_at_wib,send_at_utc,timezone,status,attempts,last_error,message_id}], pagination } | |
GET /api/schedules/:id | { timezone, data } | |
DELETE /api/schedules/:id | Hanya pending bisa cancel | { success, message:"Schedule cancelled" } |
POST /api/schedules/:id/cancel |
POST https://crm.jemberkamera.id/api/schedules
x-api-key: wg_xxx
Content-Type: application/json
{
"session_id": "cs-01",
"to": "628123456789",
"type": "text",
"text": "Halo, ini jadwal WIB!",
"send_at": "2026-08-29 21:00:00",
"media_url": "https://.../foto.jpg",
"media_mimetype": "image/jpeg"
}
// → 201 { success:true, message:"Terjadwal (WIB/GMT+7)...", data:{ id, send_at:"2026-08-29 21:00:00 WIB (GMT+7)", send_at_utc:"2026-08-29 14:00:00 UTC" } }
Worker kirim tepat saat send_at_utc <= NOW() UTC, log ke messages (out/sent) + webhook message.scheduled_sent. Gagal retry hingga 3x lalu failed.
| Method | Path |
|---|---|
GET /api/contacts/:sessionId/check/:number — onWhatsApp(jid) → { exists, jid } | |
GET /api/contacts/:sessionId/groups — groupFetchAllParticipating() | |
GET /api/contacts/:sessionId/groups/:groupId — groupMetadata(gid) | |
GET /api/contacts/:sessionId/profile-picture/:number — URL avatar |
| Method | Path |
|---|---|
GET /api/api-keys — list | |
POST /api/api-keys — { name:"my-app", rate_limit_per_minute:60, expires_at:"2026-12-31 23:59:59"|null } | |
PATCH /api/api-keys/:id — { is_active, rate_limit_per_minute, expires_at } | |
DELETE /api/api-keys/:id | |
POST /api/api-keys/:id/regenerate — rotasi key |
Key format wg_<hex48>. Buat lalu salin — tidak ditampilkan penuh lagi.
Set webhook_url per session. Event yang dikirim:
connection.update — { status:"qr"|"connected"|"disconnected"|"logged_out", jid, phone, code }message.upsert — chat masuk: { message_id, from, from_number, pushName, type, text, timestamp }message.status — { message_id, status:"delivered"|"read"|"played" }message.scheduled_sent — jadwal terkirim: { schedule_id, message_id, to }Envelope:
{
"event": "message.upsert",
"session_id": "cs-01",
"timestamp": "2026-08-29T...",
"data": { "from":"628xxx@s.whatsapp.net","from_number":"628xxx","pushName":"Budi","type":"text","text":"Halo" }
}
Header User-Agent: JemberKamera-CRM/1.0, Content-Type: application/json, retry WEBHOOK_RETRY_MAX, log webhook_logs.
| Status | Arti |
|---|---|
| 400 | Validasi (field wajib, format) |
| 401 | Missing x-api-key header (atau login via Bearer JWT) |
| 403 | Key disabled/expired, akun nonaktif |
| 404 | Session/schedule/message tidak ada |
| 409 | Session tidak connected, status tidak cocok (mis cancel non-pending) |
| 429 | Rate limit — header X-RateLimit-Limit / Remaining, body retry_after_ms |
| 503 | DB not ready (beberapa fitur butuh MySQL) |
Global IP limit RATE_LIMIT_MAX/menit + per-key rate_limit_per_minute. Format error: { success:false, message }.
JS (axios) — https://crm.jemberkamera.id
const BASE = 'https://crm.jemberkamera.id';
const KEY = 'wg_xxx';
// 1) Buat session + ambil QR
await axios.post(`${BASE}/api/sessions`, { session_id: 'cs-01', name: 'CS Jember' }, { headers:{'x-api-key':KEY} });
const { data } = await axios.get(`${BASE}/api/sessions/cs-01/qr`, { headers:{'x-api-key':KEY} });
// data.data.qr_image → <img src=qr_image>
// 2) Kirim (typing + delay 3s otomatis)
await axios.post(`${BASE}/api/messages/send-text`, {
session_id: 'cs-01', to: '628123456789', text: 'Halo!',
delay_ms: 3000, typing_ms: 2500
}, { headers:{'x-api-key':KEY} });
// 3) Media
await axios.post(`${BASE}/api/messages/send-media`, {
session_id: 'cs-01', to: '628123456789', type: 'image',
media_url: 'https://crm.jemberkamera.id/file.jpg', caption: 'Lihat ini'
}, { headers:{'x-api-key':KEY} });
// 4) Jadwal WIB (GMT+7) — vercel, jemberkamera production
await axios.post(`${BASE}/api/schedules`, {
session_id: 'cs-01', to: '628123456789',
text: 'Halo jadwal WIB!', send_at: '2026-08-29 21:00:00'
}, { headers:{'x-api-key':KEY} });
// 5) History + unread + mark-read
const hist = await axios.get(`${BASE}/api/messages?session_id=cs-01&is_read=0&limit=10`, { headers:{'x-api-key':KEY} });
const unread = await axios.get(`${BASE}/api/messages/unread-count?session_id=cs-01`, { headers:{'x-api-key':KEY} });
await axios.post(`${BASE}/api/messages/mark-read`, { session_id:'cs-01', all:true }, { headers:{'x-api-key':KEY} });
// 6) Typing manual
await axios.post(`${BASE}/api/messages/typing`, { session_id:'cs-01', to:'628123456789', duration_ms:2500 }, { headers:{'x-api-key':KEY} });
PHP (cURL) — https://crm.jemberkamera.id
$BASE='https://crm.jemberkamera.id'; $KEY='wg_xxx'; // send-text $ch=curl_init($BASE.'/api/messages/send-text'); curl_setopt_array($ch,[ CURLOPT_POST=>true, CURLOPT_HTTPHEADER=>['Content-Type: application/json','x-api-key: '.$KEY], CURLOPT_POSTFIELDS=>json_encode(['session_id'=>'cs-01','to'=>'628123456789','text'=>'Halo!','delay_ms'=>3000]), CURLOPT_RETURNTRANSFER=>true ]); echo curl_exec($ch); // schedule WIB $ch=curl_init($BASE.'/api/schedules'); curl_setopt_array($ch,[ CURLOPT_POST=>true, CURLOPT_HTTPHEADER=>['Content-Type: application/json','x-api-key: '.$KEY], CURLOPT_POSTFIELDS=>json_encode(['session_id'=>'cs-01','to'=>'628123456789','text'=>'Jadwal WIB','send_at'=>'2026-08-29 21:00:00']), CURLOPT_RETURNTRANSFER=>true ]); echo curl_exec($ch); // login JWT + pakai Bearer $ch=curl_init($BASE.'/api/auth/login'); curl_setopt_array($ch,[CURLOPT_POST=>true,CURLOPT_HTTPHEADER=>['Content-Type: application/json'],CURLOPT_POSTFIELDS=>json_encode(['username'=>'admin','password'=>'admin123']),CURLOPT_RETURNTRANSFER=>true]); $j=json_decode(curl_exec($ch),true); $tok=$j['token']; $ch=curl_init($BASE.'/api/messages?limit=5'); curl_setopt_array($ch,[CURLOPT_HTTPHEADER=>['Authorization: Bearer '.$tok],CURLOPT_RETURNTRANSFER=>true]); echo curl_exec($ch);
cURL — https://crm.jemberkamera.id
# login → JWT
curl -X POST https://crm.jemberkamera.id/api/auth/login \
-H 'Content-Type: application/json' -d '{"username":"admin","password":"admin123"}'
# → { token }
# sessions
curl https://crm.jemberkamera.id/api/sessions -H 'x-api-key: wg_xxx'
curl https://crm.jemberkamera.id/api/sessions/cs-01/qr -H 'x-api-key: wg_xxx'
# send dengan delay & typing
curl -X POST https://crm.jemberkamera.id/api/messages/send-text \
-H 'x-api-key: wg_xxx' -H 'Content-Type: application/json' \
-d '{"session_id":"cs-01","to":"628123456789","text":"Halo","delay_ms":3000,"typing_ms":2500}'
# schedule WIB (GMT+7)
curl -X POST https://crm.jemberkamera.id/api/schedules \
-H 'x-api-key: wg_xxx' -H 'Content-Type: application/json' \
-d '{"session_id":"cs-01","to":"628123456789","text":"Jadwal WIB","send_at":"2026-08-29 21:00:00"}'
# list & cancel schedule
curl "https://crm.jemberkamera.id/api/schedules?status=pending" -H 'x-api-key: wg_xxx'
curl -X DELETE https://crm.jemberkamera.id/api/schedules/123 -H 'x-api-key: wg_xxx'
# unread & mark-read
curl "https://crm.jemberkamera.id/api/messages/unread-count?session_id=cs-01" -H 'x-api-key: wg_xxx'
curl -X POST https://crm.jemberkamera.id/api/messages/mark-read \
-H 'x-api-key: wg_xxx' -H 'Content-Type: application/json' -d '{"session_id":"cs-01","all":true}'
servers: [{ url: https://crm.jemberkamera.id }]
security: [ { ApiKey: [x-api-key] }, { Bearer: [Authorization: Bearer <JWT|wg_xxx>] } ]
paths:
/health, /docs, /dashboard.html, /login.html
/api/auth/login, /api/auth/logout, /api/auth/me
/api/sessions, /api/sessions/{id}, /api/sessions/{id}/qr, /api/sessions/{id}/status, /api/sessions/{id}/webhook, /api/sessions/{id}/restart
/api/messages/send-text, /api/messages/send-media, /api/messages/send-bulk, /api/messages/typing, /api/messages/unread-count, /api/messages/mark-read, /api/messages/{id}/read, /api/messages, /api/messages/{id}
/api/schedules, /api/schedules/{id}, /api/schedules/{id}/cancel
/api/contacts/{sessionId}/check/{number}, /api/contacts/{sessionId}/groups, /api/contacts/{sessionId}/groups/{groupId}, /api/contacts/{sessionId}/profile-picture/{number}
/api/api-keys, /api/api-keys/{id}, /api/api-keys/{id}/regenerate
webhooks: message.upsert, message.status, message.scheduled_sent, connection.update
common query: ?session_id=&status=&limit=&offset=&is_read=&direction=
response envelope: { success: bool, data?: any, message?: string, pagination?: {total,limit,offset} }
Untuk OpenAPI JSON penuh, generate dari tabel path di atas. Upload ke Swagger UI jika perlu.
Import: mysql -u root -p < schema.sql atau npm run migrate (patch is_read/read_at + scheduled_messages).
| Tabel | Kolom Penting |
|---|---|
api_keys | id, name, api_key (wg_...), is_active, rate_limit_per_minute, last_used_at, expires_at |
sessions | session_id, name, phone_number, jid, status (disconnected|connecting|qr|connected|logged_out|error), qr_code, webhook_url, webhook_enabled, webhook_events |
messages | id, message_id, session_id, direction (in|out), from_jid, to_jid, to_number (generated), type, content, media_url, status, is_read, read_at, raw_payload, created_by_api_key_id |
scheduled_messages | id, session_id, to_jid, type, content, media_url, send_at_wib (WIB display), send_at_utc (UTC exec), timezone=Asia/Jakarta, status pending|sent|failed|cancelled, attempts, message_id |
webhook_logs | session_id, event, url, payload, response_code, response_body, attempt, status |
contacts | session_id, jid, name, notify, is_business, is_group |
groups | session_id, group_jid, subject, owner, participants_count, participants JSON |
admin_users | id, name, email, username, password_hash (bcrypt), role admin|operator, is_active |
ENV wajib: DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_NAME=whatsapp_gate PORT, BASE_URL=https://crm.jemberkamera.id, TZ=Asia/Jakarta, TZ_OFFSET_MINUTES=420 MASTER_API_KEY, JWT_SECRET, JWT_EXPIRES_IN=7d ADMIN_USER, ADMIN_PASS, ADMIN_EMAIL SESSIONS_DIR=./sessions, QR_PRINT_TERMINAL SEND_DELAY_MS=3000, SEND_TYPING_MS=2500 SCHEDULE_POLL_MS=15000, SCHEDULE_BATCH=20 WEBHOOK_RETRY_MAX, WEBHOOK_TIMEOUT_MS RATE_LIMIT_WINDOW_MS, RATE_LIMIT_MAX
Semua tabel InnoDB utf8mb4_unicode_ci. Foreign key session_id cascade.