Lewati ke konten
kenari.

Notifikasi

kenari memberi tahu kamu saat kuota paket hampir habis, saat paket akan berakhir, dan saat saldo menipis. Setiap peringatan muncul di lonceng pada dasbor. Kamu juga bisa mengirimnya ke Telegram atau ke webhook, jadi peringatan sampai ke kamu atau ke tool milikmu tanpa perlu membuka dasbor. Halaman ini menjelaskan apa saja yang dikirim, cara menghubungkan tiap saluran, cara memverifikasi webhook, dan cara skrip atau agen membaca angka yang sama.

Peringatan dikelompokkan dalam kategori. Setiap chat Telegram dan setiap webhook memilih kategori mana yang diterima, di Pengaturan, Notifikasi.

KategoriIsinyaAktif secara bawaan
Kuota paketJendela kuota mingguan atau bulanan mencapai ambang peringatanmu, atau habis.ya
LanggananPaket berakhir dalam 3 hari atau besok, perpanjangan otomatis berhasil atau gagal, saldo akan kurang untuk perpanjangan otomatis, paket berhenti, kupon kedaluwarsa.ya
SaldoSaldo menipis, dan kredit yang masuk ke saldo.ya
Kunci APISebuah API key mencapai batas biaya.ya
Pod dan domainKejadian pod dan domain, balasan dukungan, dan pemberitahuan saat salah satu salurannya dimatikan.ya
PengumumanPengumuman produk dari kenari.tidak

Lonceng selalu menerima semuanya, apa pun pilihanmu untuk saluran. Peringatan kuota dikirim sekali per jendela saat pemakaian mencapai ambangmu (80% kalau tidak kamu ubah), dan sekali lagi saat jendela habis. Peringatan saldo menipis dikirim saat saldo turun di bawah Rp 20.000 (kalau tidak kamu ubah). Lihat Ambang.

Di Telegram, peringatan awal, pemberitahuan paket 3 hari, perpanjangan otomatis yang berhasil, dan pengumuman datang tanpa suara. Sisanya berbunyi.

kenari mengirim paling banyak 20 peringatan per jam ke saluranmu. Kalau ada yang mengirim lebih banyak, sisanya hanya tampil di lonceng.

Kamu bisa menghubungkan sampai 5 saluran, termasuk Telegram.

  1. Buka Pengaturan di dasbor dan masuk ke Notifikasi.
  2. Di bagian Telegram, pilih Hubungkan. Ini membuka @kenarihq_bot dengan tautan sekali pakai.
  3. Tekan Start di Telegram. Bot menjawab bahwa chat sudah terhubung ke akunmu.

Tautan hanya berlaku sekali dan kedaluwarsa setelah 10 menit. Kalau sudah kedaluwarsa, pilih Hubungkan lagi. Hanya chat pribadi yang berfungsi. Pesan di grup dan channel diabaikan. Satu chat Telegram hanya milik satu akun kenari. Kalau chat itu sudah terhubung ke akun lain, kirim /stop di chat tersebut dulu, lalu buka tautan baru.

Bot mengenali empat perintah:

PerintahFungsinya
/quotaMenampilkan sisa tiap jendela kuota paketmu, lengkap dengan waktu reset.
/balanceMenampilkan saldo yang tersedia.
/stopMemutuskan chat dan menghentikan peringatan.
/helpMenampilkan daftar perintah.

Setiap peringatan punya tombol yang membuka halaman dasbor yang sesuai. Untuk memutuskan dari dasbor, pilih Putuskan di bagian Telegram. Kalau kamu memblokir bot atau menghapus chat, Telegram menolak pesannya dan kenari mematikan salurannya.

Tambahkan webhook di Pengaturan, Notifikasi, Tambah webhook. kenari mengirim pesan tes dulu dan hanya menyimpan webhook kalau servermu menjawab dengan status 2xx. Kalau tes gagal, alasannya ditampilkan dan tidak ada yang disimpan.

URL harus memakai https. kenari menolak URL yang memuat username atau password, dan URL yang mengarah ke alamat jaringan privat. kenari tidak mengikuti redirect dan menunggu jawabanmu paling lama 5 detik.

kenari memilih format dari URL.

URLFormat
https://discord.com/api/webhooks/... atau https://discordapp.com/api/webhooks/...Embed Discord dengan tautan ke halaman dasbor.
https://hooks.slack.com/...Pesan Slack dengan tombol Buka.
Selain ituJSON, bertanda tangan.

Hanya format JSON yang bertanda tangan dan punya secret. Webhook Discord dan Slack membawa tokennya sendiri di URL.

Webhook JSON menerima POST dengan content-type: application/json dan body ini:

FieldTipeKeterangan
typestringJenis peringatan, dengan titik, misalnya quota.week.warn. Lihat tabel di bawah.
idstringId notifikasi. Dikirim juga di header webhook-id dan tetap sama di setiap percobaan ulang.
created_atintegerWaktu peringatan dibuat, dalam detik Unix.
titlestringJudul singkat.
bodystringIsi pesan.
linkstringURL absolut ke halaman dasbor yang sesuai.
categorystringSalah satu dari quota, plan, balance, keys, services, announcements.
dataobjek atau nullDisediakan untuk field tambahan dari suatu jenis. Nilainya null kalau peringatan tidak punya, jadi tangani keduanya.
{
"type": "quota.week.warn",
"id": "5b7c0d0e-3f5a-4a56-9c8e-0a1d2f9e7b41",
"created_at": 1791622800,
"title": "Kuota mingguan 80% terpakai",
"body": "Sisa Rp 40.000 dari Rp 200.000, berputar 15 Okt 14:00 WIB. Setelah habis, pemakaian lanjut ditagih dari saldo.",
"link": "https://kenari.id/plan",
"category": "quota",
"data": null
}

Nilai type yang paling sering muncul:

Kategoritype
quotaquota.week.warn, quota.week.out, quota.month.warn, quota.month.out
plansub.expiring.3d, sub.expiring, autorenew.short, sub.autorenewed, sub.autorenew.failed, sub.lapsed, coupon.expired
balancewallet.low
keyskey.cap.hit
serviceschannel.disabled, test, dan kejadian pod dan domain
announcementsannouncement

Abaikan type yang tidak kamu kenali dan jawab 2xx, supaya jenis baru tidak merusak penerimamu. Pesan tes dari Kirim tes punya type test dan id yang diawali test-.

HeaderNilai
webhook-idSama dengan id di body. Pakai untuk membuang duplikat, karena percobaan ulang mengirim id yang sama.
webhook-timestampWaktu percobaan ini dikirim, dalam detik Unix. Percobaan ulang mendapat timestamp baru.
webhook-signaturev1, diikuti tanda tangan base64.

Header ini mengikuti konvensi Standard Webhooks, jadi library Standard Webhooks bisa memverifikasinya.

Verifikasi setiap request sebelum kamu mempercayainya. Tanda tangan membuktikan bahwa pesan datang dari kenari dan tidak diubah di perjalanan.

Secret penandatanganan berbentuk whsec_... dan hanya ditampilkan sekali, saat kamu menambahkan webhook JSON. Kalau hilang, pilih Buat ulang secret. Secret lama langsung berhenti berlaku, jadi perbarui penerimamu segera setelahnya.

Cara memverifikasi request:

  1. Ambil body request mentah, persis seperti yang diterima. Jangan parse lalu serialisasi ulang, karena itu mengubah byte-nya.
  2. Susun string {webhook-id}.{webhook-timestamp}.{body}.
  3. Hapus awalan whsec_ dari secret, lalu decode sisanya dari base64. Hasilnya adalah key HMAC.
  4. Hitung HMAC-SHA256 dari string itu dengan key tersebut, lalu encode hasilnya ke base64.
  5. Nilai header adalah v1, diikuti hasil itu. Bandingkan dengan hasil hitunganmu dalam waktu konstan.
  6. Tolak request kalau webhook-timestamp berselisih lebih dari 5 menit dari jam servermu. Ini mencegah pesan lama diputar ulang.
import { createHmac, timingSafeEqual } from "node:crypto";
import { createServer } from "node:http";
const TOLERANCE_SECONDS = 300;
// headers: header request Node dengan huruf kecil. rawBody: Buffer atau string persis seperti yang diterima.
export function verifyKenariWebhook(secret, headers, rawBody) {
const id = headers["webhook-id"];
const timestamp = headers["webhook-timestamp"];
const signatures = headers["webhook-signature"];
if (!id || !timestamp || !signatures) return false;
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!Number.isFinite(age) || age > TOLERANCE_SECONDS) return false;
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = createHmac("sha256", key)
.update(`${id}.${timestamp}.`)
.update(rawBody)
.digest();
// Header bisa memuat beberapa tanda tangan yang dipisah spasi, masing-masing "v1,<base64>".
for (const entry of signatures.split(" ")) {
const [version, value] = entry.split(",");
if (version !== "v1" || !value) continue;
const given = Buffer.from(value, "base64");
if (given.length === expected.length && timingSafeEqual(given, expected)) return true;
}
return false;
}
const secret = process.env.KENARI_WEBHOOK_SECRET;
createServer((req, res) => {
const chunks = [];
req.on("data", (chunk) => chunks.push(chunk));
req.on("end", () => {
const rawBody = Buffer.concat(chunks);
if (!verifyKenariWebhook(secret, req.headers, rawBody)) {
res.writeHead(401).end();
return;
}
const event = JSON.parse(rawBody.toString("utf8"));
console.log(event.type, event.title);
res.writeHead(204).end();
});
}).listen(3000);
import base64
import hashlib
import hmac
import json
import os
import time
from http.server import BaseHTTPRequestHandler, HTTPServer
TOLERANCE_SECONDS = 300
def verify_kenari_webhook(secret: str, headers, raw_body: bytes) -> bool:
msg_id = headers.get("webhook-id")
timestamp = headers.get("webhook-timestamp")
signatures = headers.get("webhook-signature")
if not msg_id or not timestamp or not signatures:
return False
try:
age = abs(time.time() - int(timestamp))
except ValueError:
return False
if age > TOLERANCE_SECONDS:
return False
key = base64.b64decode(secret.removeprefix("whsec_"))
signed = f"{msg_id}.{timestamp}.".encode() + raw_body
expected = hmac.new(key, signed, hashlib.sha256).digest()
# Header bisa memuat beberapa tanda tangan yang dipisah spasi, masing-masing "v1,<base64>".
for entry in signatures.split(" "):
version, _, value = entry.partition(",")
if version != "v1" or not value:
continue
try:
given = base64.b64decode(value)
except ValueError:
continue
if hmac.compare_digest(given, expected):
return True
return False
class Receiver(BaseHTTPRequestHandler):
def do_POST(self):
raw_body = self.rfile.read(int(self.headers.get("content-length", 0)))
secret = os.environ["KENARI_WEBHOOK_SECRET"]
if not verify_kenari_webhook(secret, self.headers, raw_body):
self.send_response(401)
self.end_headers()
return
event = json.loads(raw_body)
print(event["type"], event["title"])
self.send_response(204)
self.end_headers()
if __name__ == "__main__":
HTTPServer(("", 3000), Receiver).serve_forever()

Jawab dengan status 2xx begitu tanda tangan terbukti benar. Kerjakan proses yang lambat setelah kamu menjawab, karena kenari hanya menunggu 5 detik.

kenari menganggap jawaban 2xx sebagai terkirim. Kalau pengiriman gagal, karena timeout, error koneksi, atau status selain 2xx, kenari mencoba lagi setelah 1 menit, 5 menit, 30 menit, dan 2 jam. Kalau percobaan terakhir gagal, peringatan itu dibuang dari saluran dan tetap ada di lonceng. Telegram mengikuti jadwal yang sama.

Sebuah saluran mematikan dirinya dalam dua kasus:

  • Pengiriman gagal 5 kali berturut-turut. Satu keberhasilan mengatur ulang hitungannya.
  • Servermu menjawab 404 atau 410, yang memberi tahu kenari bahwa endpoint-nya sudah tidak ada. Saluran langsung mati.

Saat saluran mati, kenari menambahkan pemberitahuan ke lonceng beserta alasannya dan tidak mengirim apa pun lagi ke saluran itu. Setelah penyebabnya kamu perbaiki, pilih Aktifkan lagi pada saluran tersebut.

Kirim tes mengirim pesan tes ke sebuah saluran saat itu juga dan menampilkan hasilnya. Tes tidak dihitung sebagai kegagalan yang bisa mematikan saluran. Kirim tes dan Tambah webhook berbagi batas 10 pengiriman per menit untuk akunmu. Lewat dari itu, kenari menjawab bahwa percobaannya terlalu banyak, dan kamu bisa mencoba lagi sebentar lagi.

Di Pengaturan, Notifikasi, Ambang kamu mengatur kapan peringatan dikirim:

PengaturanPilihanBawaan
Ingatkan saat kuota paket mencapai50%, 80%, atau 90% dari jendela kuota80%
Ingatkan saat saldo di bawahJumlah Rupiah bulat sampai Rp 10.000.000. 0 mematikan peringatan saldo.Rp 20.000

Mengubah ambang kuota di tengah jendela tidak mengirim peringatan kedua untuk jendela itu. Peringatan saldo dikirim sekali saat saldomu turun di bawah jumlah itu, dan baru dikirim lagi setelah saldo diisi hingga di atas jumlah itu lalu turun lagi. Peringatan saldo menipis hanya dikirim ke akun yang membayar dari saldo dalam 14 hari terakhir atau yang perpanjangan otomatisnya aktif.

Skrip atau agen bisa memeriksa anggarannya tanpa membuka dasbor.

Balasan sukses dari /v1/chat/completions, /v1/messages, dan /v1/responses bisa membawa dua header, baik streaming maupun tidak:

HeaderKapan munculNilai
x-kenari-plan-remainingKey adalah key tanpa pembatasan milik akunmu sendiri dan paketmu aktif.Pecahan sisa tiap jendela kuota, misalnya week=0.19, month=0.70. Tiap nilai berkisar 0 sampai 1 dengan 2 desimal. Jendela tanpa plafon tidak ditampilkan.
x-kenari-balance-micro-idrRequest ditagih dari saldo.Saldo tersedia dalam micro-Rupiah, dengan Rp 1 sama dengan 1.000.000. Ini saldo dikurangi dana yang ditahan request yang sedang berjalan, angka yang sama dengan available_micro_idr di Saldo akun.

x-kenari-plan-remaining menunjukkan keadaan sebelum request ini ditagih. API key dibagikan dan key yang dibatasi tidak pernah mendapatkannya. Sebuah balasan hanya membawa header yang syaratnya terpenuhi.

Anggap keduanya sebagai petunjuk, bukan jaminan. Saat balasan non-streaming memakan waktu sangat lama, kenari mengirim status 200 lebih awal dan menjaga koneksi tetap terbuka selagi model bekerja. Header dari balasan yang sudah selesai tidak bisa ditambahkan lagi setelah itu, jadi balasan non-streaming yang sangat lama bisa tiba tanpa kedua header ini. Balasan streaming dan balasan dengan durasi normal membawanya. Kalau kamu butuh angka yang pasti, panggil Kuota akun atau Saldo akun.

Terminal window
curl -si https://kenari.id/v1/chat/completions \
-H "Authorization: Bearer $KENARI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "step-3-7-flash:free", "messages": [{"role": "user", "content": "Halo!"}]}' \
| grep -i '^x-kenari-'

GET /v1/account/quota mengembalikan angka yang sama dengan lebih rinci. Setiap jendela punya budget_rp (plafonnya dalam Rupiah utuh) dan used_frac (pecahan yang terpakai, dari 0 sampai 1 dengan 4 desimal), dan level atas punya alert_pct, ambang peringatan kuotamu. Agen bisa berhenti sendiri saat used_frac * 100 mencapai alert_pct. Lihat Kuota akun.

kenari status --line mencetak satu baris berisi pemakaian kuota dan saldo, lalu keluar dengan kode 0. Perintah ini dibuat untuk status line Claude Code dan untuk prompt shell:

kuota mgg 81% · bln 40% · saldo Rp 12.400

Baris ini menampilkan persentase terpakai dari setiap jendela yang punya plafon, lalu saldo tersedia. Kalau tidak ada key yang diatur, perintah mencetak baris kosong, jadi prompt tidak pernah rusak. Saat kenari tidak terjangkau, baris tetap menampilkan bacaan terakhir sampai 24 jam dan mencoba lagi sekali per menit. Hasilnya di-cache selama 60 detik, dan pengambilan data menunggu paling lama 3 detik. Key harus berupa key tanpa pembatasan milik akunmu sendiri, sama seperti untuk endpoint kuota.

Status line berjalan setiap kali prompt digambar ulang, jadi atur KENARI_API_KEY di shell kamu, jangan mengandalkan keychain sistem yang bisa meminta sandi atau menunggu keyring yang terkunci.

Untuk menampilkannya di Claude Code, tambahkan ini ke ~/.claude/settings.json:

{
"statusLine": {
"type": "command",
"command": "kenari status --line"
}
}

Saat kamu menjalankan tool lewat CLI kenari dan sebuah jendela kuota sudah mencapai atau melewati ambang peringatanmu, CLI juga mencetak satu baris peringatan sebelum tool dijalankan. Peringatan ini hanya membaca bacaan yang sudah di-cache dan tidak pernah menunggu jaringan. Lihat CLI kenari.