Cara Kirim Notifikasi Live Chat ke Slack dan Telegram (Juga Feishu dan DingTalk)
Tim Anda sudah sehari-hari di grup chat. Ini forwarder kecil yang menangkap webhook live chat, memeriksa signature, lalu mengirim setiap pertanyaan baru ke Slack atau Telegram, juga Feishu dan DingTalk.
Untuk mendapatkan notifikasi live chat di Slack atau Telegram, juga Feishu dan DingTalk, Anda menaruh satu program kecil di antara SealReply dan grup tim: program itu menerima webhook, memverifikasi signature, lalu mengirim ringkasan satu baris ke URL bot grup. Seluruh forwarder hanya satu file Node tanpa dependensi, sekitar 50 baris untuk satu platform dan sedikit di atas seratus baris untuk keempatnya.
Ringkasan
- Keempat platform mengizinkan Anda mengirim pesan ke grup lewat incoming webhook atau token bot; tidak perlu review aplikasi atau OAuth.
- Forwarder memverifikasi header
X-Signatureterhadap raw body, langsung membalas 200, dan melakukan de-duplikasi berdasarkanX-SealReply-Event-Id. - Cukup berlangganan dua peristiwa: pesan baru dari pengunjung atau agen, dan pesan offline dari pengunjung.
- Feishu dan DingTalk menandatangani request bot dengan cara yang berlawanan; tertukar di antara keduanya adalah alasan paling umum notifikasi tidak sampai.
- Webhook adalah fitur berbayar (mulai paket Starter). Jika Anda hanya butuh pemberitahuan, notifikasi push sama sekali tidak butuh kode.
Di bawah ini: cara mendapatkan URL bot keempat platform, forwarder-nya sendiri, tempat menjalankannya, dan pertimbangan agar grup tidak berubah menjadi penuh kebisingan.

Kenapa notifikasi live chat dikirim ke Slack atau Telegram, bukan memantau dashboard?
Karena orang yang bisa menjawab pertanyaan sulit sudah ada di grup chat, bukan di konsol. Dashboard chat ditujukan untuk orang yang tugasnya menjawab chat. Tetapi orang yang menjawab pertanyaan sulit biasanya tidak ada di sana: developer, orang yang mengemas pesanan, pemilik usaha. Saat pengunjung bertanya "bisa kirim ke Kanada?", Anda ingin kalimat itu sampai ke tempat orang-orang tersebut sudah berada.
Dua hal langsung Anda dapatkan begitu itu terjadi. Siapa pun yang sedang senggang menjawab duluan, tanpa ada yang perlu menugaskan. Dan grup menyimpan catatan, jadi menggulir ke atas di malam hari sudah cukup untuk tahu apakah ada yang terlewat.
Webhook ada di paket berbayar
Webhook butuh paket Starter atau di atasnya. Jika Anda hanya ingin diberi tahu bahwa ada yang bertanya, Anda tidak butuh semua ini: aplikasi mobile, notifikasi push, dan jam operasional sudah mencukupinya, seperti dijelaskan di cara tidak melewatkan pelanggan saat Anda offline. Webhook dipakai saat peristiwa harus sampai ke grup Anda, atau ke sistem Anda sendiri.
Sebelum mulai, Anda butuh tiga hal:
- Ruang kerja SealReply dengan paket Starter atau di atasnya, dan widget chat yang sudah terpasang di situs Anda.
- Tempat untuk menjalankan program Node 18+ kecil dengan alamat HTTPS publik: VPS kecil, atau fungsi serverless.
- Hak admin di workspace Slack, grup Telegram, grup Feishu, atau grup DingTalk yang ingin Anda kirimi pesan.
Bagaimana mendapatkan URL bot untuk Slack, Telegram, Feishu, dan DingTalk?
Keempat platform punya endpoint "kirim ke ruangan ini" yang tidak butuh review aplikasi maupun OAuth. Yang Anda dapatkan berbeda: Slack dan Feishu memberi URL lengkap, DingTalk memberi URL ditambah token, sedangkan Telegram memberi token bot dan Anda masih harus mencari chat ID sendiri.
Slack
Panduan incoming webhooks Slack menjelaskannya secara lengkap; versi singkatnya: buka api.slack.com/apps, pilih Create New App → From scratch, beri nama dan pilih workspace. Di sidebar, klik Incoming Webhooks, aktifkan, lalu Add New Webhook to Workspace, pilih channel, dan beri izin. URL-nya muncul kembali di halaman itu, bentuknya seperti https://hooks.slack.com/services/T…/B…/….
Untuk mengirim ke channel privat, Anda sendiri harus menjadi anggota channel tersebut.
Telegram
Kirim pesan ke @BotFather, ketik /newbot, dan ikuti petunjuknya (panduan BotFather dari Telegram menjelaskan setiap langkah). Anda akan mendapat token seperti 123456:AA…. Tambahkan bot ke grup Anda, kirim pesan apa saja di sana, lalu buka:
https://api.telegram.org/bot<your-token>/getUpdates
chat.id di respons itu adalah chat ID Anda. Untuk grup dan channel nilainya negatif, seperti -1001234567890, jadi salin juga tanda minusnya.
Feishu
Di grup, buka Settings → Group bots → Add bot → Custom bot. Beri nama, lalu pilih minimal satu opsi di Security settings. Verifikasi signature adalah pilihan yang tepat; opsi ini memberi Anda sebuah rahasia. (Alternatifnya, kata kunci wajib, berarti setiap pesan yang Anda kirim harus mengandung kata itu.) URL-nya seperti https://open.feishu.cn/open-apis/bot/v2/hook/<token>. Panduan custom bot Feishu mencantumkan opsi keamanan dan aturan penandatanganannya.
DingTalk
Di grup, buka Group settings → Bots → Add bot → Custom. Sama seperti Feishu: minimal satu opsi keamanan (kata kunci, signature, atau rentang IP). Signature memberi Anda rahasia berawalan SEC; halaman robot security settings DingTalk menjelaskan ketiga opsi. URL-nya seperti https://oapi.dingtalk.com/robot/send?access_token=….
Setiap platform mengharapkan body request yang berbeda, jadi forwarder menyusun satu bentuk per platform:
| Platform | Body |
|---|---|
| Slack | {"text": "…"} |
| Telegram | {"chat_id": "-100…", "text": "…"} |
| Feishu | {"msg_type": "text", "content": {"text": "…"}} |
| DingTalk | {"msgtype": "text", "text": {"content": "…"}} |
Apa yang dilakukan script forwarder?

Empat tugas: verifikasi, de-duplikasi, ubah peristiwa menjadi satu kalimat, lalu kirim dalam bentuk yang diminta tiap platform. Node 18 atau lebih baru, tanpa npm install, satu file forward.mjs.
Verifikasi lebih dulu. Endpoint webhook adalah URL publik yang bisa di-POST siapa saja, jadi setiap request diperiksa terhadap body mentah, dan apa pun yang timestamp-nya meleset lebih dari lima menit dibuang:
import { createServer } from "node:http";
import { createHmac, timingSafeEqual } from "node:crypto";
const SECRET = process.env.SEALREPLY_WEBHOOK_SECRET;
const APP = "https://app.sealreply.com";
const seen = new Map(); // event id -> when it arrived, for de-duplication
function verify(header, raw) {
const p = Object.fromEntries((header ?? "").split(",").map((x) => x.split("=", 2)));
if (!p.v1 || !/^\d+$/.test(p.t ?? "") || Math.abs(Date.now() / 1000 - Number(p.t)) > 300) return false;
const mine = Buffer.from(createHmac("sha256", SECRET).update(`${p.t}.`).update(raw).digest("hex"));
const theirs = Buffer.from(p.v1);
return mine.length === theirs.length && timingSafeEqual(mine, theirs);
}
Alasan kode ini ditulis begitu, beserta fungsi yang sama dalam Python, PHP, dan Go, ada di cara verifikasi signature webhook. Satu hal yang perlu diulang di sini: verifikasi terhadap byte yang Anda terima. Menjalankan JSON.parse lalu JSON.stringify untuk mendapatkannya kembali menghasilkan string yang berbeda dan digest yang berbeda.
Berikutnya, mengubah peristiwa menjadi kalimat. Hanya dua peristiwa yang dibutuhkan: message.created dan offline_message.created. Perhatikan bahwa message.created juga terpicu untuk balasan tim Anda sendiri, jadi saring berdasarkan sender.type; kalau tidak, setiap jawaban yang diketik agen akan muncul di grup:
// Turn an event into one notification. null means "do not forward".
function notice(ev) {
if (ev.type === "message.created") {
const { message: m, conversation: c } = ev.data;
if (m.sender.type !== "visitor") return null; // only what the visitor said
return {
title: "New chat",
body: m.text ?? `[${m.content_type}]`,
who: `Visitor ${c.visitor_id.slice(0, 8)}`,
label: "Open chat",
link: `${APP}/conversations/${c.id}`,
};
}
if (ev.type === "offline_message.created") {
const o = ev.data.offline_message;
return {
title: "Offline message",
body: o.content,
who: `${o.name ?? "Visitor"} ${o.email}`,
label: "Open message",
link: `${APP}/offline-messages`,
};
}
return null;
}
link adalah bagian paling berharga dari notifikasi: satu ketukan langsung membuka percakapan itu, bukan sebuah daftar. Jika pengunjung belum memberikan nama, delapan karakter pertama visitor_id cukup sebagai penanda, cukup untuk melihat bahwa tiga pesan berturut-turut datang dari orang yang sama.
Lalu pengirimannya. Setiap platform diaktifkan oleh environment variable-nya sendiri, jadi Anda bisa mulai dari satu platform dan menambah yang lain nanti:
const post = (url, body) =>
fetch(url, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
const plain = (n) => `${n.title} · ${n.who}\n${n.body}\n${n.link}`;
// Slack needs & < > escaped, or it reads them as its own link syntax
const slackSafe = (s) => s.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
async function fanOut(n) {
const jobs = [];
if (process.env.SLACK_WEBHOOK_URL) {
jobs.push(post(process.env.SLACK_WEBHOOK_URL, {
text: `*${slackSafe(n.title)}* · ${slackSafe(n.who)}\n${slackSafe(n.body)}\n<${n.link}|${n.label}>`,
}));
}
if (process.env.TELEGRAM_TOKEN) {
jobs.push(post(`https://api.telegram.org/bot${process.env.TELEGRAM_TOKEN}/sendMessage`, {
chat_id: process.env.TELEGRAM_CHAT_ID,
text: plain(n),
disable_web_page_preview: true,
}));
}
if (process.env.FEISHU_WEBHOOK_URL) {
const body = { msg_type: "text", content: { text: plain(n) } };
if (process.env.FEISHU_SECRET) {
const ts = Math.floor(Date.now() / 1000).toString(); // seconds
// Feishu: "<timestamp>\n<secret>" is the HMAC key, the message is empty
body.timestamp = ts;
body.sign = createHmac("sha256", `${ts}\n${process.env.FEISHU_SECRET}`).digest("base64");
}
jobs.push(post(process.env.FEISHU_WEBHOOK_URL, body));
}
if (process.env.DINGTALK_WEBHOOK_URL) {
let url = process.env.DINGTALK_WEBHOOK_URL;
if (process.env.DINGTALK_SECRET) {
const ts = Date.now().toString(); // milliseconds
// DingTalk: the secret is the key, "<timestamp>\n<secret>" the message, base64 then URL-encoded
const sign = createHmac("sha256", process.env.DINGTALK_SECRET)
.update(`${ts}\n${process.env.DINGTALK_SECRET}`).digest("base64");
url += `×tamp=${ts}&sign=${encodeURIComponent(sign)}`;
}
jobs.push(post(url, { msgtype: "text", text: { content: plain(n) } }));
}
for (const r of await Promise.allSettled(jobs)) {
if (r.status === "rejected") console.error("forward failed", r.reason);
else if (!r.value.ok) console.error("forward failed", r.value.status, await r.value.text());
}
}
Feishu dan DingTalk menandatangani dengan cara berlawanan
Keduanya terlihat mirip, padahal tidak. Di sinilah sebagian besar waktu terbuang; detailnya di bawah.
Feishu memakai "<timestamp>\n<secret>" sebagai kunci HMAC atas pesan kosong, dengan timestamp dalam detik, dan menaruh signature di body. DingTalk memakai rahasia sebagai kunci dan "<timestamp>\n<secret>" sebagai pesan, dengan milidetik, di-base64 lalu di-URL-encode ke query string. Menyalin salah satunya sambil membaca dokumentasi yang lain menghasilkan signature yang tidak cocok, tanpa petunjuk lain yang berguna.
Terakhir, servernya. Dua hal penting: balas sebelum bekerja, dan de-duplikasi berdasarkan ID peristiwa:
createServer((req, res) => {
const chunks = [];
req.on("data", (c) => chunks.push(c));
req.on("end", async () => {
const raw = Buffer.concat(chunks);
if (req.method !== "POST" || !verify(req.headers["x-signature"], raw)) return res.writeHead(401).end();
res.writeHead(200).end(); // acknowledge first, forward after
const id = req.headers["x-sealreply-event-id"];
if (seen.has(id)) return;
seen.set(id, Date.now());
for (const [k, t] of seen) if (Date.now() - t > 36e5) seen.delete(k);
try {
const n = notice(JSON.parse(raw.toString("utf8")));
if (n) await fanOut(n);
} catch (e) {
console.error("handling the event failed", e);
}
});
}).listen(process.env.PORT ?? 8080);
SealReply menunggu 2xx apa pun dalam 10 detik, lalu mencoba ulang setelah 1 menit, 5 menit, 30 menit, 2 jam, 6 jam, dan 24 jam, dan mematikan endpoint yang gagal tiga hari berturut-turut. Menunggu empat panggilan keluar sebelum membalas adalah cara mudah melewati batas 10 detik, jadi 200 dikirim lebih dulu.
De-duplikasi memakai header X-SealReply-Event-Id, yang nilainya tetap sama di setiap retry dan kirim ulang manual. Map di memori di atas menyimpan data satu jam dan kosong saat restart; itu cukup untuk "jangan kirim dua kali", yang memang satu-satunya tujuannya.
Bagaimana mengaktifkan webhook di SealReply?
Di konsol, buka Developer → Webhook → Tambah endpoint:
- URL endpoint: alamat HTTPS publik server Anda, misalnya
https://hooks.yourdomain.com/sealreply. Rentang alamat privat danlocalhostditolak. - Peristiwa: centang dua saja, "Pengunjung atau agen mengirim pesan" dan "Pengunjung meninggalkan pesan offline". Lebih sedikit peristiwa, lebih sedikit baris tak berguna di grup.
- Salin rahasia penandatanganan (
whsec_…); rahasia ini hanya ditampilkan sekali. Hilang? Rahasia baru menerbitkan rahasia lain dan langsung mematikan yang lama.
Lalu klik Kirim peristiwa uji, yang mengirim ping. ping bukan salah satu dari dua jenis yang ditangani script, jadi tidak ada yang muncul di grup, tetapi Log pengiriman seharusnya menunjukkan 200. Log itu menyimpan pengiriman selama 30 hari lengkap dengan status, kode respons, waktu yang dibutuhkan, percobaan berikutnya, dan body persis yang dikirim; kebanyakan pertanyaan sudah terjawab di sana. Untuk melihat pesan grup sungguhan, buka situs Anda sendiri dan ajukan pertanyaan.
Satu ruang kerja bisa punya 10 endpoint, jadi jika Anda mengelola banyak situs, pisahkan per situs di dalam script, jangan menambah satu endpoint per situs.
Di mana forwarder sebaiknya dijalankan?
VPS termurah yang bisa Anda temukan sudah cukup; pemakaian memorinya hanya beberapa megabyte. Taruh script di /opt/sealreply-forward/forward.mjs dan rahasianya di file env yang hanya bisa dibaca root:
# /etc/sealreply-forward.env (chmod 600)
SEALREPLY_WEBHOOK_SECRET=whsec_…
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/…
TELEGRAM_TOKEN=123456:AA…
TELEGRAM_CHAT_ID=-1001234567890
Unit systemd menjaganya tetap berjalan setelah reboot maupun crash:
# /etc/systemd/system/sealreply-forward.service
[Unit]
Description=SealReply webhook forwarder
After=network.target
[Service]
ExecStart=/usr/bin/node /opt/sealreply-forward/forward.mjs
EnvironmentFile=/etc/sealreply-forward.env
Restart=always
User=nobody
[Install]
WantedBy=multi-user.target
systemctl enable --now sealreply-forward menjalankannya; journalctl -u sealreply-forward -f mengikuti log-nya.
TLS menjadi tugas apa pun yang ada di depannya. Caddy paling sedikit repotnya: tiga baris, dan sertifikatnya diurus sendiri:
hooks.yourdomain.com {
reverse_proxy 127.0.0.1:8080
}
Jangan biarkan proxy menyentuh body. Menulis ulang, mengompres ulang, atau meng-encode ulang body akan merusak signature. Caddy dan Nginx tidak mengubahnya secara default, tetapi jika Anda mengaktifkan sesuatu seperti sub_filter, kecualikan path ini.
Di fungsi serverless
Jika Anda tidak mau merawat server, logika yang sama bisa berjalan di Cloudflare Workers, Vercel, atau layanan function-as-a-service apa pun, dengan dua penyesuaian.
Ambil raw body. Runtime senang mem-parse JSON untuk Anda. Cari method yang memberikan byte mentah, misalnya await request.text() di Workers, atau bodyParser dimatikan di runtime Node Vercel; kalau tidak, signature Anda tidak akan pernah cocok.
Pikirkan ulang de-duplikasi. Instance datang dan pergi, jadi Map di memori tidak bisa diandalkan. Simpan ID peristiwa di KV store dengan masa berlaku satu jam, atau lewati de-duplikasi sama sekali: pengiriman ganda jarang terjadi, dan satu baris berulang di grup chat lebih murah daripada sebuah datastore.
Bagaimana agar grup chat tidak terlalu berisik?

Begitu forwarding berjalan, keluhan berikutnya biasanya grup terlalu ramai dan orang-orang sudah membisukan bot. Yang benar-benar membantu:
- Hanya pesan pertama. Satu percakapan bisa berisi belasan pesan, tetapi hanya pesan pembuka yang perlu mengganggu orang. Periksa
conversation.status: nilainyawaitingsampai seseorang di tim Anda membalas secara publik. Teruskan hanya selama statusnyawaiting, dan setiap percakapan menghasilkan tepat satu baris. - Hanya yang tidak tertangani. Jika ada yang memantau dashboard di siang hari, lewati
message.createdsama sekali dan berlanggananoffline_message.createdsaja, supaya grup menutup jam-jam saat Anda tidak ada. - Ruangan berbeda. Pertanyaan pra-penjualan ke channel sales, laporan error ke channel engineering: pilih URL bot berbeda berdasarkan
conversation.site_id, atau berdasarkan kata kunci di pesan. - Jangan kirim seluruh percakapan. Hak akses grup chat biasanya lebih longgar daripada konsol Anda. Potong body menjadi beberapa ratus karakter dan biarkan link membawa sisanya, terutama ketika pengunjung meninggalkan alamat email dan nomor telepon.
Poin terakhir itu layak mendapat penjelasan tersendiri. Di konsol, data pengunjung disimpan terenkripsi dengan kunci tersendiri untuk setiap pelanggan; begitu Anda meneruskannya ke channel berisi 200 orang, satu-satunya pelindungnya adalah daftar anggota channel itu. "Ada yang bertanya" ditambah ringkasan satu baris jauh lebih aman untuk diteruskan daripada transkrip lengkap.
Bagaimana jika notifikasi tidak sampai?
Periksa dengan urutan ini; biasanya masalahnya ketemu di langkah pertama atau kedua:
- Kode status apa yang ada di log pengiriman? 401 berarti verifikasi Anda menolaknya (daftar troubleshooting di artikel tentang signature membahas penyebab umumnya). 5xx atau timeout berarti script Anda error. Tertahan di "Mencoba ulang" berarti tidak ada yang bisa dijangkau; periksa DNS dan firewall.
- Log menunjukkan 200 tapi grup sepi? Berarti panggilan keluar yang gagal; cari "forward failed" di
journalctl. Penyebab umum: signature Feishu dan DingTalk tertukar; DingTalk diatur ke mode kata kunci tetapi teksnya tidak mengandung kata kunci itu; chat ID Telegram kehilangan tanda minus; URL Slack yang sudah dibuat ulang. - Hanya balasan agen Anda sendiri yang muncul? Filter
sender.typetidak berjalan. Pastikan Anda membacaev.data.message.sender.type. - Semua terkirim dua kali? De-duplikasi tidak berfungsi. Nama header di Node datang dalam huruf kecil, jadi baca
x-sealreply-event-id.
Pertanyaan umum
Bisakah membalas pengunjung situs web dari Slack atau Telegram?
Tidak, forwarder ini satu arah: hanya mengirim notifikasi dan sebuah link. Ketuk link itu untuk membuka percakapan di konsol SealReply atau aplikasi mobile, lalu balas dari sana. Membalas di SealReply juga menjaga detail pengunjung tetap terenkripsi di satu tempat, bukan tersebar di berbagai grup chat.
Apakah perlu paket berbayar untuk notifikasi live chat di Slack atau Telegram?
Ya, untuk cara ini. Webhook butuh paket Starter atau di atasnya. Notifikasi push ke ponsel dan komputer, serta aplikasi mobile SealReply, tidak butuh webhook dan tidak butuh kode.
Apakah forwarder butuh server sendiri?
Anda butuh tempat yang bisa menerima request HTTPS dari internet. VPS termurah sudah cukup, begitu juga fungsi serverless seperti Cloudflare Workers atau Vercel, asalkan Anda bisa membaca raw body request. localhost dan alamat jaringan privat ditolak sebagai URL endpoint.
Bagaimana cara menemukan chat ID grup Telegram?
Tambahkan bot ke grup, kirim pesan apa saja di sana, lalu buka https://api.telegram.org/bot<your-token>/getUpdates di browser. chat.id di respons itu adalah ID grup. Untuk grup dan channel nilainya negatif, seperti -1001234567890, dan tanda minus adalah bagian dari ID.
Kenapa Feishu atau DingTalk bilang signature tidak cocok?
Biasanya karena kedua metode penandatanganan tertukar. Feishu memakai "<timestamp>\n<secret>" sebagai kunci HMAC atas pesan kosong dengan timestamp dalam detik; DingTalk memakai rahasia sebagai kunci dan "<timestamp>\n<secret>" sebagai pesan, dengan milidetik, lalu hasilnya di-URL-encode. Periksa juga jam server: Feishu menolak timestamp yang selisihnya lebih dari satu jam dari waktu servernya.
Amankah meneruskan pesan pengunjung ke grup chat?
Seaman daftar anggota grup itu. Di konsol SealReply, data pengunjung disimpan terenkripsi dengan kunci tersendiri untuk setiap pelanggan; di grup chat, siapa pun di grup bisa membaca dan meneruskannya. Kirim ringkasan singkat dan link, bukan transkrip lengkap, email, dan nomor telepon.
Langkah selanjutnya
Setelah forwarder berjalan, peristiwa lain juga layak dicoba: lead.updated bisa mengirim prospek ke CRM Anda sendiri, visitor.identified bisa memicu email sambutan begitu seseorang meninggalkan alamatnya, conversation.closed bisa mengarsipkan percakapan harian ke data warehouse. Daftar peristiwa beserta field-nya ada di dokumentasi developer.
Jika widget chat belum terpasang di situs Anda, mulai dari sana lalu kembali ke forwarder ini: mulai menggunakan SealReply hanya butuh sekitar lima menit, dan live chat untuk bisnis kecil membahas seluruh jalur dari pemasangan sampai penjualan berhasil.