Domain produksi: https://api.crm.emerald-system.id · Dev: · Auth: x-api-key: wg_xxx atau Authorization: Bearer <JWT> · Login · Dashboard · Health
Semua contoh pakai https://api.crm.emerald-system.id — lokal ganti http://localhost:3000. Setiap kirim ada typing composing → paused + delay 3 detik (WIB = GMT+7 untuk jadwal).
BASE_URL=https://api.crm.emerald-system.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=jk_gateway MASTER_API_KEY=wg_xxx JWT_SECRET=..._min_32_chars ADMIN_USER=admin ADMIN_PASS=... ADMIN_EMAIL=admin@emerald-system.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://api.crm.emerald-system.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.jk.local. Nol depan jadi 62. Group: xxx@g.us.
https://api.crm.emerald-system.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://api.crm.emerald-system.id/api/auth/login
Content-Type: application/json
{ "username":"admin", "password":"admin123" }
// → { token:"eyJ...", user:{ username:"admin", email:"admin@emerald-system.id" } }
GET https://api.crm.emerald-system.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://api.crm.emerald-system.id/hook", webhook_enabled:true } → 201 { session_id, status:"connecting"|"qr" }. session_id dinormalkan [a-z0-9_-]. |
PUT /api/sessions/:id | Edit session — body (semua opsional): { name, webhook_url, webhook_enabled, webhook_events, phone_number }. Ganti phone_number → validasi 1-nomor-1-sesi (409 jika sudah dipakai session lain), koneksi direset + file auth dihapus → scan QR baru dengan nomor tersebut (relogin_required:true). History pesan lama tetap aman. |
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://api.crm.emerald-system.id/api/sessions
x-api-key: wg_xxx
{ "session_id":"cs-01", "name":"CS" }
GET https://api.crm.emerald-system.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://api.crm.emerald-system.id, TZ=Asia/Jakarta.
POST /api/messages/send-text
POST https://api.crm.emerald-system.id/api/messages/send-text
x-api-key: wg_xxx
Content-Type: application/json
{
"session_id": "cs-01",
"to": "628123456789",
"text": "Halo dari CRM Gateway!",
"user": "Admin CS",
"quoted_message_id": "optional-MSG_ID",
"typing": true,
"typing_ms": 2500,
"delay_ms": 3000
}
// → { success:true, data:{ message_id, to:"628123456789@s.jk.local", to_number:"628123456789", user:"Admin CS" } }
Override: typing:false = tanpa typing, delay_ms:0 = tanpa delay. Field user (opsional) = nama penulis pesan, diisi oleh client — tersimpan di kolom messages.user dan muncul di GET /api/messages.
POST /api/messages/send-media — multipart file atau JSON URL (typing + delay sama, auto-detect tipe jika kosong)
POST https://api.crm.emerald-system.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, user, 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://api.crm.emerald-system.id/file.jpg","caption":"Lihat ini","user":"Admin CS","typing":true,"delay_ms":3000 }
// field user (opsional) = nama penulis dari client → kolom messages.user
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","user":"Admin CS","typing_ms":2000,"delay_ms":3000 },
{ "to":"628222","text":"Halo 2","user":"Admin Marketing" }
]
}
// → { success:true, data:[{to, to_number, success, message_id, user|error}] } // user opsional per item → messages.user, balik di response per item
GET /api/messages?session_id=cs-01&direction=in|out&status=sent|delivered|read|failed|received|pending&is_read=0|1&user=Admin%20CS&type=text|image|video|audio|document|sticker&q=keyword&limit=20&offset=0 — history DB. GET ini sudah kembalikan semua: text + image + video + audio + document + sticker (jangan khawatir, bukan hanya text). is_read butuh migrate (fallback otomatis). Filter user exact, type bisa image,video atau image,video,document (comma), q search di content/media_url/to_jid/from_jid. PENTING: filter utama adalah session_id — tanpa itu akan scan semua session. Jika total:0 cek: session_id harus persis sama saat POST, DB harus connected, dan jangan salah type (text vs image). Semua tipe termasuk video/image/document/audio pasti kebaca di GET (kolom type + media_url + media_mimetype + media_filename + content=caption).
Field penting tiap row: user — nama penulis pesan: chat masuk = pushName pengirim; chat keluar = dikirim client via field user di POST send-text/send-media/send-bulk. response_ms (ms sejak pesan masuk pertama yang belum terbalas — hanya diisi di pesan keluar yang merupakan balasan; NULL untuk kirim proaktif/campaign), group_jid via webhook. Untuk pesan grup: from_jid = nomor pengirim asli (peserta, LID sudah dipetakan ke nomor), to_jid = JID grup. Rata-rata waktu balas per session: SELECT AVG(response_ms/1000) FROM messages WHERE response_ms IS NOT NULL GROUP BY session_id.
GET https://api.crm.emerald-system.id/api/messages?session_id=cs-01&limit=5
x-api-key: wg_xxx
// → { success:true, data:[
// { "id":540, "direction":"in", "from_jid":"628123456789@s.jk.local", "user":"Budi", "response_ms":null, "type":"text", "content":"halo min", "media_url":null, "media_mimetype":null, "media_filename":null },
// { "id":541, "direction":"out", "from_jid":"628990909119:8@s.jk.local", "user":"Admin CS", "response_ms":8234, "type":"text", "content":"Halo! Bisa kak", "media_url":null },
// { "id":542, "direction":"in", "type":"image", "content":"caption foto masuk", "media_url":"uploads/media/cs-01/ABCD1234.jpg", "media_mimetype":"image/jpeg", "media_filename":"ABCD1234.jpg", "status":"received" },
// { "id":543, "direction":"in", "type":"video", "content":"caption video masuk", "media_url":"uploads/media/cs-01/EFGH5678.mp4", "media_mimetype":"video/mp4", "media_filename":"EFGH5678.mp4" },
// { "id":544, "direction":"in", "type":"document", "content":"caption pdf", "media_url":"uploads/media/cs-01/IJKL9012.pdf", "media_mimetype":"application/pdf", "media_filename":"laporan.pdf" },
// { "id":545, "direction":"in", "type":"audio", "content":"", "media_url":"uploads/media/cs-01/MNOP3456.ogg", "media_mimetype":"audio/ogg; codecs=opus", "media_filename":"MNOP3456.ogg" },
// { "id":546, "direction":"out", "type":"image", "content":"caption foto keluar", "media_url":"uploads/media/cs-01/QRST7890.jpg", "media_mimetype":"image/jpeg", "media_filename":"foto.jpg" }
// ], pagination:{ total, limit, offset } }
// GET ini kembalikan SEMUA tipe: text + image + video + document + audio + sticker — bukan hanya text
// Filter media saja: GET /api/messages?session_id=cs-01&type=image atau ?type=image,video,document → semua row media pasti ada media_url/mimetype/filename
// Ambil binary: GET /api/messages/542/media?api_key=wg_xxx → image jpeg; GET /api/messages/544/media?api_key=wg_xxx → pdf (inline/preview)
// Jika total:0 → (1) cek session_id persis "cs-01" (case-sensitive), (2) GET tanpa filter coba ?limit=5, (3) cek DB: SELECT COUNT(*) FROM messages WHERE session_id='cs-01'
GET /api/messages/:id — detail satu row (termasuk media_url, media_mimetype, media_filename jika media)
Semua pesan media yang masuk (image, video, audio, document, sticker) otomatis di-download via downloadMediaMessage Baileys dan disimpan ke uploads/media/<session_id>/<messageId>.ext di server. Kolom DB terisi: type (image/video/…), content (caption), media_url (relative path uploads/media/…), media_mimetype, media_filename. Pesan keluar via send-media (upload file) juga di-persist agar bisa di-preview ulang.
Dashboard /dashboard.html → panel History Pesan — media langsung: image tampil thumbnail (klik untuk lightbox zoom), video pakai <video controls> bisa diputar langsung, audio <audio>, dokumen tampil card dengan tombol preview / download. Semua load via endpoint di bawah dengan auth yang sama.
| Method | Path | Deskripsi |
|---|---|---|
GET /api/messages/:id/media | Stream file langsung. :id = messages.id (numeric) atau message_id (string). Auth: Authorization: Bearer JWT / x-api-key via header atau query ?token=JWT / ?api_key=wg_xxx (wajib untuk <img src> / <video>).Jika media_url eksternal https://… → 302 redirect. Header: Content-Type dari media_mimetype, Content-Disposition: inline; filename="…" (tambah ?download=1 jadi attachment), Accept-Ranges: bytes untuk streaming video. Cache max-age=86400. | Image/video/audio/document/sticker — semua via satu endpoint |
POST /api/messages/:id/retry-media | Re-download manual jika file hilang / gagal download awal. Butuh raw_payload tersimpan & session connected. Cek uploads/media/… dulu — jika sudah ada balas sudah tersedia. | Pakai saat history muncul tapi file 404 |
GET /api/messages?session_id=…&type=image | Filter history media saja. type=image,video (comma) didukung. Tiap row sudah ada media_url, media_mimetype, media_filename + content (caption). | Discovery sebelum fetch file |
Path storage: uploads/media/<session_id>/<messageId>.jpg|.mp4|.pdf|… (ext dari mimetype). Dibuat otomatis, tidak perlu konfigurasi. Untuk send-media via media_url eksternal, DB simpan URL aslinya dan /media akan redirect.
Contoh — tampilkan gambar dari chat:
GET https://api.crm.emerald-system.id/api/messages?session_id=cs-01&type=image&limit=5
x-api-key: wg_xxx
// → data[0] = { id: 541, type:"image", content:"caption", media_url:"uploads/media/cs-01/ABCD1234.jpg", media_mimetype:"image/jpeg", media_filename:"foto.jpg" }
// Tampilkan langsung di browser (img / video / audio / document)
GET https://api.crm.emerald-system.id/api/messages/541/media?api_key=wg_xxx
// Header: x-api-key: wg_xxx ATAU query ?api_key=wg_xxx ATAU Authorization: Bearer <JWT> ?token=...
// → binary image/jpeg, bisa dipakai:
// HTML
<img src="https://api.crm.emerald-system.id/api/messages/541/media?api_key=wg_xxx" alt="foto" />
<video controls src="https://api.crm.emerald-system.id/api/messages/542/media?api_key=wg_xxx"></video>
<audio controls src="https://api.crm.emerald-system.id/api/messages/543/media?api_key=wg_xxx"></audio>
<a href="https://api.crm.emerald-system.id/api/messages/544/media?api_key=wg_xxx&download=1">Download dokumen</a>
// JS (fetch dengan JWT)
const token = localStorage.getItem('jk_token');
document.querySelector('img').src = `/api/messages/${row.id}/media?token=${encodeURIComponent(token)}`;
// cURL
curl "https://api.crm.emerald-system.id/api/messages/541/media?api_key=wg_xxx" --output foto.jpg
curl "https://api.crm.emerald-system.id/api/messages/544/media?api_key=wg_xxx&download=1" --output dokumen.pdf
curl -H "x-api-key: wg_xxx" https://api.crm.emerald-system.id/api/messages/541/media --output foto.jpg
// Jika 404 (file belum ke-download), retry:
curl -X POST https://api.crm.emerald-system.id/api/messages/541/retry-media -H "x-api-key: wg_xxx"
// → { success:true, message:"Media berhasil di-download ulang" }
Auth untuk <img>/<video>: browser tidak bisa set header, jadi pakai ?token=JWT atau ?api_key=wg_xxx. Dashboard sudah otomatis append query tersebut. Cookie token dari login juga ikut terkirim jika pakai credentials: include. Jika tanpa auth → 401. Jika file tidak ada → 404 { message:"Media file tidak ditemukan…" }.
Realtime: event message.upsert (webhook & Socket.IO) untuk media sekarang include media_url, media_mimetype, media_filename, db_id selain type, text, from — bisa langsung build URL /api/messages/{db_id}/media.
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:[MSG_ID], ids:[dbId], jid:"628xxx@s.jk.local", 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", user:"Admin CS" } — user opsional → messages.user saat terkirim; 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,user,type,content,media_url,send_at_wib,send_at_utc,timezone,status,attempts,last_error,message_id}], pagination } | Field user = penulis (sama seperti messages.user) |
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://api.crm.emerald-system.id/api/schedules
x-api-key: wg_xxx
Content-Type: application/json
// Text saja
{
"session_id": "cs-01",
"to": "628123456789",
"type": "text",
"text": "Halo, ini jadwal WIB!",
"user": "Admin CS",
"send_at": "2026-08-29 21:00:00"
}
// → 201 { success:true, message:"Terjadwal (WIB/GMT+7)...", data:{ id, user:"Admin CS", send_at:"2026-08-29 21:00:00 WIB (GMT+7)", send_at_utc:"2026-08-29 14:00:00 UTC" } }
// Gambar terjadwal — client tinggal kasih media_url, server yang atur decode & kirim
// Saat terkirim, GET /api/messages akan kasih media_url langsung pakai, client tinggal fetch
{
"session_id": "cs-01",
"to": "628123456789",
"type": "image",
"text": "Halo, ini jadwal WIB! Lihat foto ini",
"user": "Admin CS",
"send_at": "2026-08-29 21:00:00",
"media_url": "https://api.crm.emerald-system.id/file.jpg",
"media_mimetype": "image/jpeg"
}
// → 201 { success:true, message:"Terjadwal...", data:{ id:123, type:"image", media_url:"https://api.crm.emerald-system.id/file.jpg", ... } }
// Setelah terkirim, cek history:
// GET /api/messages?session_id=cs-01&type=image
// → data[0] = { id: 668, type:"image", content:"Halo, ini jadwal WIB! Lihat foto ini", media_url:"https://api.crm.emerald-system.id/api/messages/668/media", media_mimetype:"image/jpeg", media_filename:"file.jpg", ... }
// Client tinggal: fetch(data[0].media_url) atau data[0].media_direct_url → langsung dapat binary image/jpeg
// Atau untuk jadwal yang belum terkirim: GET /api/schedules?session_id=cs-01 → data[0].media_url langsung URL yang akan dikirim
Gambar di schedules: cukup kirim type:"image" + media_url (https) + media_mimetype — server simpan, kirim via Baileys dengan typing+delay, lalu di messages hasilnya media_url sudah jadi direct URL /api/messages/:id/media (atau tetap https jika external) jadi client tinggal fetch(media_url) tanpa decode. Untuk video/document sama, ganti type jadi video/document.
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 — onDevice(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 (sama untuk webhook & realtime):
connection.update — { status:"qr"|"connected"|"disconnected"|"logged_out", jid, phone, code }message.upsert — chat masuk: { message_id, from, from_number, group_jid?, pushName, type, text, timestamp } — from selalu nomor pengirim asli (LID `@lid` otomatis dipetakan ke `62xxx@s.jk.local`); untuk pesan grup, group_jid berisi JID grup (xxx@g.us).message.status — { message_id, status:"delivered"|"read"|"played" }message.waiting — decrypt retry: { message_id, from, reason:"decrypt_retry_cached" }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.jk.local","from_number":"628xxx","group_jid":"628212xxx-1495xxx@g.us","pushName":"Budi","type":"text","text":"Halo" }
}
Header User-Agent: CRM-Gateway/1.0, Content-Type: application/json, retry WEBHOOK_RETRY_MAX, log webhook_logs. Event yang sama juga dikirim via Realtime jika client join room.
Realtime via Socket.IO 4.x di wss://api.crm.emerald-system.id/socket.io (lokal ws://localhost:3010). Auth & event sama seperti webhook — butuh join room per session.
Auth (wajib): query ?token=JWT atau ?api_key=wg_xxx atau header Authorization: Bearer <JWT> / x-api-key: wg_xxx. Gagal = connect_error: UNAUTHORIZED.
| Emit client → server | Payload | Balasan |
|---|---|---|
join | { session_id:"cs-01" } atau "cs-01" | { success, room:"session:cs-01" } |
subscribe | { sessions:["cs-01","marketing"] } | { success, rooms } |
leave | { session_id:"cs-01" } | { success } |
ping | - | { pong, at } |
Event server → client: message.upsert, connection.update, message.status, message.waiting, message.scheduled_sent. Diterima di 3 channel (pilih salah satu):
socket.on("message.upsert", cb)socket.on("message", cb) — semua eventsocket.on("event", cb) — aliasFormat: { event, session_id, timestamp, data } — sama persis dengan webhook envelope. Harus sudah join room session tersebut.
import { io } from "socket.io-client";
const s = io("https://api.crm.emerald-system.id", {
path: "/socket.io",
transports: ["websocket","polling"],
query: { api_key: "wg_xxx" } // atau token: "eyJ..."
});
s.on("connect", () => s.emit("join", { session_id: "cs-01" }, console.log));
s.on("message.upsert", m => console.log(m.session_id, m.data.text));
s.on("connection.update", m => console.log(m.data.status));
s.on("connect_error", e => console.error(e.message));
Gunakan socket.io-client@^4.7. Detail: /docs/realtime.md. Realtime & webhook dikirim bersamaan — webhook untuk server-to-server, socket untuk browser/consumer live.
Visual flow builder di /agent-studio.html — framework LangGraph + DeepAgent (hybrid dalam satu flow, nggak ada LangChain/simple). Tiap node LLM punya prompt sendiri (system + user per-node, reasoning per-node), provider central di Pengaturan.
trigger.device (Pesan masuk), trigger.manual (API/cron)llm.prompt, llm.reasoning — reasoning low|medium|high|xhigh, temperature per-nodetool.knowledge (RAG .md), tool.api (GET/POST), tool.mysql (direct, pilih koneksi saved atau DB utama)logic.if (contains/equals/regex, branch true/false), transform.template ({{input}} {{from}} {{knowledge}} {{db_rows}} {{api_text}} {{output}})tool.wa_send (to {{from}} atau 628xxx), output.replyLangGraph vs DeepAgent: LangGraph = sekuensial step-prompt simpel; DeepAgent = node reasoning multi-step/loop untuk analisa mendalam (research, compare). Bisa digabung hybrid dalam satu canvas.
DB Direct: buat koneksi di Pengaturan → Database (host/port/database/user/pass), test, lihat tabel/kolom, preview query. Node MySQL pilih koneksi saved (kosong = DB utama).
Wizard AI: tanya-jawab 2-3 langkah + generate flow 1 klik, bisa pilih .md dari Knowledge atau upload .md API — wizard baca endpoint/method/auth autentik dari dokumen untuk node API.
| Method | Path | Deskripsi |
|---|---|---|
POST /api/agents | Body: { name, type:"langgraph"|"deepagent", flow_json:{nodes,edges}, device_session_id, trigger_keywords, knowledge_ids, llm_provider_id } | Buat agent |
GET /api/agents | ?session_id=cs-01&q=cs | List |
GET /api/agents/:id | Detail | |
PUT /api/agents/:id | Isi sama seperti POST | Update |
DELETE /api/agents/:id | Hapus | |
POST /api/agents/:id/run | { input, session_id?, from?, dryRun } | Eksekusi flow → { output, vars, traces, executed, latency_ms } |
POST /api/agents/:id/test | { input } | Test dryRun |
GET /api/agents/:id/runs?limit=10 | History eksekusi | |
GET /api/llm-providers | List provider (openrouter/ollama/openai/custom, reasoning low..xhigh) | |
POST /api/llm-providers | { name, type, base_url?, api_key, model, reasoning_effort, temperature } | Buat provider |
GET /api/knowledge | ?q=faq | List .md |
POST /api/knowledge | { title, filename, content } atau multipart file | Upsert / upload .md |
POST /api/db-connections | { name, host, port, database, user, password } | Simpan koneksi direct |
POST /api/db-connections/preview-query | { connection_id, sql, params } | Preview query |
POST /api/agent-wizard/chat | { message, history, provider_id?, context, doc_ids?, uploaded_mds? } | Wizard tanya-jawab |
POST /api/agent-wizard/generate | { prompt, provider_id?, type?, context, doc_ids?, uploaded_mds? } | Generate flow 1 klik |
POST /api/agent-wizard/upload-md | multipart files: .md | Upload .md untuk wizard |
Docs: BASE_URL=https://api.crm.emerald-system.id, api_key: wg_xxx atau Authorization: Bearer JWT. Flow vars: {{input}} {{from}} {{knowledge}} {{api_text}} {{db_rows}} {{output}}.
| 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://api.crm.emerald-system.id
const BASE = 'https://api.crm.emerald-system.id';
const KEY = 'wg_xxx';
// 1) Buat session + ambil QR
await axios.post(`${BASE}/api/sessions`, { session_id: 'cs-01', name: 'CS' }, { 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 text dengan user (typing + delay 3s otomatis)
await axios.post(`${BASE}/api/messages/send-text`, {
session_id: 'cs-01', to: '628123456789', text: 'Halo!', user: 'Admin CS',
delay_ms: 3000, typing_ms: 2500
}, { headers:{'x-api-key':KEY} });
// → { success:true, data:{ message_id, to, to_number, user:"Admin CS" } }
// 3) Media dengan user
await axios.post(`${BASE}/api/messages/send-media`, {
session_id: 'cs-01', to: '628123456789', type: 'image',
media_url: 'https://api.crm.emerald-system.id/file.jpg', caption: 'Lihat ini', user: 'Admin CS'
}, { headers:{'x-api-key':KEY} });
// → { success:true, data:{ message_id, to, type, user } }
// 4) Jadwal WIB (GMT+7) — dengan user
await axios.post(`${BASE}/api/schedules`, {
session_id: 'cs-01', to: '628123456789',
text: 'Halo jadwal WIB!', user: 'Admin CS', 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://api.crm.emerald-system.id
$BASE='https://api.crm.emerald-system.id'; $KEY='wg_xxx';
// send-text dengan user
$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!','user'=>'Admin CS','delay_ms'=>3000]),
CURLOPT_RETURNTRANSFER=>true
]);
echo curl_exec($ch); // → { success:true, data:{ message_id, to, to_number, user:"Admin CS" } }
// GET history — lihat user di tiap row
$ch=curl_init($BASE.'/api/messages?session_id=cs-01&limit=2');
curl_setopt_array($ch,[CURLOPT_HTTPHEADER=>['x-api-key: '.$KEY],CURLOPT_RETURNTRANSFER=>true]);
echo curl_exec($ch); // → data[].user = "Admin CS" (out) / pushName (in)
// schedule WIB dengan user
$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','user'=>'Admin CS','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://api.crm.emerald-system.id
# login → JWT
curl -X POST https://api.crm.emerald-system.id/api/auth/login \
-H 'Content-Type: application/json' -d '{"username":"admin","password":"admin123"}'
# → { token }
# sessions
curl https://api.crm.emerald-system.id/api/sessions -H 'x-api-key: wg_xxx'
curl https://api.crm.emerald-system.id/api/sessions/cs-01/qr -H 'x-api-key: wg_xxx'
# send dengan delay & typing + user
curl -X POST https://api.crm.emerald-system.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","user":"Admin CS","delay_ms":3000,"typing_ms":2500}'
# → { success:true, data:{ message_id, to, to_number, user:"Admin CS" } }
# GET history — cek user
curl "https://api.crm.emerald-system.id/api/messages?session_id=cs-01&limit=2" -H 'x-api-key: wg_xxx'
# → data[0].user = "Admin CS" (out) / "Budi" (in, pushName)
# schedule WIB (GMT+7) dengan user
curl -X POST https://api.crm.emerald-system.id/api/schedules \
-H 'x-api-key: wg_xxx' -H 'Content-Type: application/json' \
-d '{"session_id":"cs-01","to":"628123456789","text":"Jadwal WIB","user":"Admin CS","send_at":"2026-08-29 21:00:00"}'
# list & cancel schedule
curl "https://api.crm.emerald-system.id/api/schedules?status=pending" -H 'x-api-key: wg_xxx'
curl -X DELETE https://api.crm.emerald-system.id/api/schedules/123 -H 'x-api-key: wg_xxx'
# unread & mark-read
curl "https://api.crm.emerald-system.id/api/messages/unread-count?session_id=cs-01" -H 'x-api-key: wg_xxx'
curl -X POST https://api.crm.emerald-system.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://api.crm.emerald-system.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/messages/{id}/media, /api/messages/{id}/retry-media
/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=&user=&type=&q=
// type: text|image|video|audio|document (comma: image,video), q: search content/media_url/jid
response envelope: { success: bool, data?: any, message?: string, pagination?: {total,limit,offset} }
POST body common: { session_id, to, text, user? } — field user opsional (string ≤255) → kolom messages.user / scheduled_messages.user, balik di response data.user dan GET list
GET note: semua type (text/image/video/audio/document/sticker) tersimpan dengan type + content(caption) + media_url/media_mimetype/media_filename → GET /api/messages memang sudah kembalikan data image/video/document lain (bukan hanya text); filter ?type=image atau ?type=video atau ?type=image,video,document untuk media saja; total:0 = cek session_id exact & DB connected. Untuk binary file pakai /media endpoint di bawah
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, user/response_ms, to_number VARCHAR(64) + mapping nomor grup, scheduled_messages — idempotent).
| 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, user (nama penulis — masuk: pushName; keluar: dari field user di POST), response_ms (waktu balas), to_jid, to_number (generated, nomor saja), type (text|image|video|audio|document|sticker), content (caption/text), media_url (uploads/media/… untuk incoming yang auto-download, atau URL/path untuk outgoing), media_mimetype, media_filename, status, is_read, read_at, raw_payload, created_by_api_key_id — GET sudah kembalikan semua tipe (image/video/document/audio semua ada di GET yang sama) |
scheduled_messages | id, session_id, to_jid, user (nama penulis dari field user POST schedule → ikut ke messages.user saat terkirim), 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=jk_gateway PORT, BASE_URL=https://api.crm.emerald-system.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.