JemberKamera CRM — API Documentation

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

Domain & ENV — Wajib Produksi

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.

Daftar Isi

Quick Start

  1. Copy .env.example.env, isi BASE_URL=https://crm.jemberkamera.id, DB_*, MASTER_API_KEY, JWT_SECRET, TZ=Asia/Jakarta.
  2. npm install lalu npm run migrate — buat is_read/read_at + scheduled_messages. Atau mysql -u root -p < schema.sql.
  3. npm start (prod) / npm run dev. Cek /healthdb: connected.
  4. Login /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).
  5. Kirim: 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.

Base URL aktif:https://crm.jemberkamera.id· prod wajib HTTPS

Auth — JWT (panel) + x-api-key (integrasi)

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.

MethodPathBodyRespon
POST /api/auth/loginpublic{ username|email, password }{ success, token, user:{id,name,email,username,role} } + Set-Cookie token
POST /api/auth/logout--clear cookie
GET /api/auth/meJWT-{ 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>

Base & Health

MethodPathAuthDeskripsi
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.htmlJWTDashboard modern (select session, kirim, jadwal WIB, keys)
GET/login.html-Login panel

Sessions — 1 nomor = 1 session

Dashboard pakai dropdown session dari GET /api/sessions — tidak perlu ketik manual. Session aktif tersimpan dan sync ke form kirim & jadwal.

MethodPath
POST /api/sessionsBody: { 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/sessionsList gabungan DB + live. { session_id, name, phone_number, jid, status, live_status, has_qr, webhook_url, webhook_enabled }
GET /api/sessions/:idDetail + 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/webhookBody: { webhook_url, webhook_enabled:bool, webhook_events:["message.upsert","connection.update"] }
POST /api/sessions/:id/restartRestart socket
DELETE /api/sessions/:idLogout + 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>

Messages — Send Text / Media / Bulk (delay 3s + typing)

Human-like: tiap send otomatis typing composingpaused 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

Read Flag — Chat Masuk

Semua chat masuk disimpan ke messages dengan direction="in", status="received", is_read=0 (unread). Chat keluar is_read=1. Ditambah read_at.

MethodPathDeskripsi
GET /api/messages/unread-count?session_id=cs-01{ total, by_session:[{session_id,cnt}] }Hitung unread
POST /api/messages/mark-readBody 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/readSatu DB id jadi read + receipt
GET /api/messages?is_read=0&direction=inList unread saja

EVENT messages.upsert → auto sendReadReceipt untuk non-group; status update via messages.update (2=delivered,3=read,4=played) + webhook message.status.

Typing — Composing / Paused

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.

Schedules — Kirim Terjadwal WIB (GMT+7)

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.

MethodPathKet
POST /api/schedulesWIB: 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/:idHanya 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.

Contacts & Groups

MethodPath
GET /api/contacts/:sessionId/check/:numberonWhatsApp(jid){ exists, jid }
GET /api/contacts/:sessionId/groupsgroupFetchAllParticipating()
GET /api/contacts/:sessionId/groups/:groupIdgroupMetadata(gid)
GET /api/contacts/:sessionId/profile-picture/:number — URL avatar

API Keys

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

Webhook

Set webhook_url per session. Event yang dikirim:

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.

Errors & Rate Limit

StatusArti
400Validasi (field wajib, format)
401Missing x-api-key header (atau login via Bearer JWT)
403Key disabled/expired, akun nonaktif
404Session/schedule/message tidak ada
409Session tidak connected, status tidak cocok (mis cancel non-pending)
429Rate limit — header X-RateLimit-Limit / Remaining, body retry_after_ms
503DB not ready (beberapa fitur butuh MySQL)

Global IP limit RATE_LIMIT_MAX/menit + per-key rate_limit_per_minute. Format error: { success:false, message }.

Contoh Lengkap — crm.jemberkamera.id

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}'

Spesifikasi API — Ringkas (pseudo-OpenAPI)

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.

Schema & Tabel — MySQL utf8mb4

Import: mysql -u root -p < schema.sql atau npm run migrate (patch is_read/read_at + scheduled_messages).

TabelKolom Penting
api_keysid, name, api_key (wg_...), is_active, rate_limit_per_minute, last_used_at, expires_at
sessionssession_id, name, phone_number, jid, status (disconnected|connecting|qr|connected|logged_out|error), qr_code, webhook_url, webhook_enabled, webhook_events
messagesid, 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_messagesid, 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_logssession_id, event, url, payload, response_code, response_body, attempt, status
contactssession_id, jid, name, notify, is_business, is_group
groupssession_id, group_jid, subject, owner, participants_count, participants JSON
admin_usersid, 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.