Cara bikin chatbot WhatsApp pakai ChatGPT API
Ada orang kirim “halo” ke nomor WhatsApp-mu, dan nomor itu balas sendiri dengan teks dari model. Cuma itu yang dikejar tulisan ini: satu pesan masuk, satu balasan keluar.
WhatsApp-nya tetap di Fonnte, sementara modelnya dipanggil lewat Chat Completions yang kompatibel OpenAI, jadi yang kamu butuh cuma key kn-, base URL kenari, dan satu model :free.
Ini bukan tulisan n8n. Kalau workflow-mu sudah telanjur di kanvas, Arahkan n8n ke kenari yang kamu butuhkan, karena host di sini cuma satu: webhook inbound Fonnte yang diarahkan ke POST /v1/chat/completions.
Satu pesan dulu, jangan produk
Godaan pertamanya bikin katalog produk dan dashboard pelanggan sekalian. Target hari ini satu saja: kamu kirim “halo” ke nomor yang terhubung, dan nomor itu membalas teks dari model. Selama itu belum jalan, fitur yang kamu tumpuk cuma jadi tumpukan error.
Satu hal soal Fonnte perlu diluruskan dulu. Mereka memang mendokumentasikan fitur AI (menu flow, kuota AI, AI data), dan Aksita adalah integrasi terpisah (node flow + API key Aksita), bukan AI bawaan itu. Update 10 Maret 2026 merencanakan deprecation AI data & kuota AI pada 1 Juni 2026 dan merekomendasikan aksita.ai, sementara halaman kuota/AI data masih ada. Yang jelas, itu semua bukan ChatGPT API. Pada tiga halaman yang kami sitir (webhook Node, webhook PHP, send API), docs nggak memuat field custom GPT atau endpoint OpenAI (API key, base URL, model) yang bisa kamu isi kn- dan https://kenari.id/v1. Ini bukan audit seluruh docs mereka, tapi kalau layar itu nggak ketemu, jangan dikarang.
Yang mereka dokumentasikan itu webhook inbound: pesan masuk di-POST ke URL milikmu, lalu kamu yang memanggil kenari dan membalas lewat https://api.fonnte.com/send.
Host: webhook Fonnte, bukan fitur AI Fonnte
Field di bawah kami ambil dari docs Fonnte, bukan dari tebakan dashboard:
- Webhook Node: Webhook reply message with nodejs
- Webhook PHP (URL publik, letak input): Make whatsapp bot using PHP (webhook)
- Kirim balasan: Sending API Messages
Yang kepakai di tulisan ini:
| Arah | Field / nilai yang tertulis |
|---|---|
| Body webhook | sender, message (juga device, name, inboxid, dan lainnya) |
| URL kirim | https://api.fonnte.com/send |
| Header kirim | Authorization: TOKEN (token langsung, tanpa Bearer) |
| Body kirim | target, message |
Urutan di docs PHP: buat device, salin token, hubungkan device dulu, baru tempel URL publik ke input webhook di device → edit, lalu nyalakan autoread yang letaknya di bawah input tadi. Satu hal yang gampang kelewat, autoreply Fonnte nggak jalan begitu webhook aktif. URL webhook juga harus publik seperti contoh di docs mereka, https://fonnte.com/urlwebhook.php, jadi localhost nggak cukup.
Tes kenari dulu, sebelum webhook
Kalau curl ke kenari saja belum berhasil, Fonnte nggak ada hubungannya, jadi beresin ini duluan.
- Masuk dashboard, buka API keys, lalu klik Buat key. Nilainya diawali
kn-dan cuma tampil sekali, jadi langsung salin. Satu key berlaku untuk semua model, dan kamu boleh punya lebih dari satu, masing-masing berlabel. - Lewati dulu urusan saldo, karena langkah ini nggak menagih apa pun.
- Ambil id yang tersedia dari katalog publik, karena
GET /v1/modelsnggak butuh key. Docs bilang daftar tanpa query isinya chat (embedding, rerank, dan moderasi lewat?modality=), sementara snapshot 21 Agu 2026 juga memuat id image/audio/music.
curl https://kenari.id/v1/models
Dari response-nya, cari id berakhiran :free atau pricing.free: true, dan jangan dihafalkan, soalnya katalognya bergerak terus. Saat tulisan ini dicek, step-3-7-flash:free masih ada dengan pricing.free: true dan endpoints yang memuat chat.
Sekarang uji key-mu, ganti kn-... di bawah dengan nilai yang kamu salin.
curl https://kenari.id/v1/chat/completions \
-H "Authorization: Bearer kn-..." \
-H "Content-Type: application/json" \
-d '{
"model": "step-3-7-flash:free",
"messages": [{"role": "user", "content": "Halo dari WhatsApp"}]
}'
Begitu choices[0].message.content ada isinya, request-nya berhasil dan sisanya urusan WhatsApp. kenari juga menerima header x-api-key, tapi Bearer cukup untuk langkah ini.
Satu jebakan yang hampir semua orang kena: base URL klien OpenAI-compat harus tepat https://kenari.id/v1, bukan https://kenari.id dan bukan https://kenari.id/v1/v1, karena dua yang salah tadi biasanya berakhir 405. Kalau curl ini sudah jawab, baru pasang webhook.
Webhook yang mem-POST ke kenari
Isinya satu file Node, dengan token Fonnte dan key kenari di env dan bukan di kode, soalnya key yang telanjur ter-commit itu urusannya panjang. Nama env di bawah cuma contoh (FONNTE_TOKEN, KENARI_API_KEY, KENARI_MODEL).
export FONNTE_TOKEN="token-device-fonnte"
export KENARI_API_KEY="kn-..."
export KENARI_MODEL="step-3-7-flash:free"
const express = require("express");
const app = express();
app.use(express.json());
const FONNTE_TOKEN = process.env.FONNTE_TOKEN;
const KENARI_KEY = process.env.KENARI_API_KEY;
const MODEL = process.env.KENARI_MODEL || "step-3-7-flash:free";
async function chat(text) {
const res = await fetch("https://kenari.id/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${KENARI_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: MODEL,
messages: [
{ role: "system", content: "Kamu asisten WhatsApp yang ringkas." },
{ role: "user", content: text },
],
}),
});
const data = await res.json();
if (!res.ok) {
throw new Error(res.status + " " + JSON.stringify(data));
}
return data.choices[0].message.content;
}
async function sendFonnte(target, message) {
const res = await fetch("https://api.fonnte.com/send", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: FONNTE_TOKEN,
},
body: JSON.stringify({ target, message }),
});
console.log(await res.json());
}
app.post("/webhook", async function (req, res) {
const sender = req.body.sender;
const message = req.body.message;
res.end();
if (!sender || !message) return;
try {
const reply = await chat(String(message));
await sendFonnte(sender, reply);
} catch (err) {
console.error(err);
}
});
app.listen(3000, function () {
console.log("webhook di :3000/webhook");
});
Bentuk sendFonnte mengikuti contoh Node Fonnte: POST https://api.fonnte.com/send, header Authorization berisi token langsung, body target plus message, dan target diisi req.body.sender. Contoh mereka nggak mengirim inboxid, dan menurut docs send, field itu dipakai untuk membalas pesan inbox (aktifkan inbox di device → edit). Bot ini cukup POST target = sender.
Ada satu perbedaan kecil yang sengaja kami ambil. Contoh Node Fonnte memanggil send dulu baru res.end(), sementara update 12 Januari 2026 menulis webhook harus mengembalikan HTTP 200 supaya dianggap sukses, dan docs nggak mewajibkan ACK sebelum kamu memanggil model. Contoh di atas res.end() duluan supaya request webhook nggak menahan koneksi selama model memproses, jadi balasan WhatsApp datang setelah model selesai.
Sisanya tinggal pasang URL publik di input webhook device Fonnte (contoh Node memakai path /webhook, contoh PHP memakai file URL, jadi path-nya tidak diwajibkan), nyalakan autoread, lalu kirim “halo” ke nomor yang terhubung. Docs yang kami sitir nggak mensyaratkan pengirimnya harus nomor lain dan bukan nomor device. Kalau balasannya muncul, tesnya berhasil.
Sesudah satu balasan berhasil
Nggak perlu top up di run pertama, karena :free ditagih Rp 0. Baru kalau kamu ganti ke id berbayar (tanpa :free) sementara saldo Rp 0, kenari mengembalikan HTTP 402 insufficient_balance, dan itu urusan billing, bukan key rusak.
Satu kn- tadi tetap kepakai, yang berubah cuma isi field model. Kalau bot-mu lebih dari satu, bikin key berlabel per bot, jadi yang bocor tinggal dicabut satu.
Kalau mentok
- Webhook sepi: URL belum publik, path nggak cocok dengan yang kamu tempel (contoh Node pakai
/webhook), device belum terhubung, atau autoread masih mati. Log body-nya (sender,message) seperti contoh Fonnte. - kenari sudah jawab tapi balasannya nggak terkirim: token Fonnte salah, atau
Authorization-nya kamu tambahiBearer. Docs Fonnte minta token langsung. 401dari kenari:kn--nya salah, kepotong waktu disalin, atau malah masihsk-. Body 401 teks polos.model_not_found: id salah ketik, atau id-nya sudah nggak di katalog. Ambil lagi dariGET /v1/models. (Tabel error kenari menyebut HTTP 400, jadi jangan andalkan 404.)405: base URL dobel/v1atau malah hilang/v1. Harushttps://kenari.id/v1.402 insufficient_balance: model berbayar sementara saldo Rp 0. Balik ke:free, atau isi saldo dulu.429: kena batas model:free. BacaRetry-After-nya, dan jangan hafalkan angkanya.
Kalau yang mentok ternyata request paling dasarnya, bukan webhook-nya, lebih cepat mundur dulu ke request pertama dari nol sampai satu curl jalan. Soal memilih id gratis yang benar, termasuk flag mana yang harus kamu baca, ada di coba dulu pakai model :free.