CRM Gateway — API Documentation

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

Domain & ENV — Wajib Produksi

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.

Daftar Isi

Quick Start

  1. Copy .env.example → .env, isi BASE_URL=https://api.crm.emerald-system.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 /health → db: 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.jk.local. Nol depan jadi 62. Group: xxx@g.us.

Base URL aktif:https://api.crm.emerald-system.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://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>

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://api.crm.emerald-system.id/hook", webhook_enabled:true } → 201 { session_id, status:"connecting"|"qr" }. session_id dinormalkan [a-z0-9_-].
PUT /api/sessions/:idEdit 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/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://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>

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

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)

Media — Ambil Gambar / Video / Dokumen Langsung

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.

MethodPathDeskripsi
GET /api/messages/:id/mediaStream 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-mediaRe-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=imageFilter 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.

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:[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/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", 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/:idHanya 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.

Contacts & Groups

MethodPath
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

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 (sama untuk webhook & realtime):

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 — Socket.IO

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 → serverPayloadBalasan
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):

Format: { 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.

Agent Studio — Visual Flow (LangGraph + DeepAgent)

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.

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

MethodPathDeskripsi
POST /api/agentsBody: { 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=csList
GET /api/agents/:idDetail
PUT /api/agents/:idIsi sama seperti POSTUpdate
DELETE /api/agents/:idHapus
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=10History eksekusi
GET /api/llm-providersList 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=faqList .md
POST /api/knowledge{ title, filename, content } atau multipart fileUpsert / 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-mdmultipart files: .mdUpload .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}}.

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 — api.crm.emerald-system.id

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

Spesifikasi API — Ringkas (pseudo-OpenAPI)

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.

Schema & Tabel — MySQL utf8mb4

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

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, 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_messagesid, 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_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=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.