Ringkasan
Ada dua layanan, masing-masing dengan kunci dan alamat sendiri. Keduanya dipanggil dari server Anda dengan cara yang sama, dan keduanya bisa dipasang di website maupun bot.
Rupiah, masuk ke saldo
Kunci sk_live_ / sk_test_ dari halaman merchant. Dana ditahan 2 hari kerja, lalu bisa ditarik.
USDT, langsung ke akun Anda
Kunci bk_live_ / bk_test_ dari menu Binance Pay. Fitur berlangganan.
Alur dasar
Buat transaksi
Satu panggilan POST dari server Anda mengembalikan QR atau nominal USDT, beserta tautan halaman bayar.
Tampilkan ke pembeli
Arahkan ke pay_url, tampilkan QR sendiri, atau kirim sebagai foto di chat bot.
Tunggu kabar lunas
Kami mengirim webhook saat status berubah. Tanpa webhook, cek status lewat API.
Bentuk jawaban
// Berhasil { "status": "success", "data": { ... } } // Gagal { "status": "error", "message": "..." }
Autentikasi
Kirim kunci API di header Authorization. Awalan kunci menentukan layanan dan modenya.
Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
Kunci sk_ hanya berlaku di endpoint QRIS dan kunci bk_ hanya di endpoint /binance. Kunci yang salah jenis dijawab 401.
Simpan kunci di server. Jangan menaruhnya di JavaScript browser, aplikasi mobile, atau kode bot yang dibagikan ke orang lain.
Mode uji
Kunci uji membuat transaksi contoh yang tidak memengaruhi saldo dan tidak memanggil penyedia pembayaran. Webhook tetap dikirim, dengan is_sandbox: true, sehingga seluruh alur bisa dicoba tanpa uang sungguhan.
Untuk melunasi transaksi uji, buka pay_url lalu tekan tombol simulasi, atau panggil:
QRIS
Pembayaran Rupiah. Biaya 0,7% + Rp 200 per transaksi lunas, dan dananya masuk ke saldo Anda.
Buat transaksi
curl -X POST https://kodebayar.id/api/v1/transactions \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "amount": 100000, "reference_id": "INV-1042", "customer_name": "Budi Santoso", "description": "Pesanan #1042", "return_url": "https://toko-anda.com/terima-kasih" }'
{
"status": "success",
"data": {
"trx_id": "TRX261004A8F3K2M9QX",
"reference_id": "INV-1042",
"channel": "qris",
"status": "pending",
"amount": 100000,
"fee": 900,
"fee_bearer": "customer",
"total_amount": 100900,
"net_amount": 100000,
"pay_url": "https://kodebayar.id/pay/TRX261004A8F3K2M9QX",
"qr_url": "https://kodebayar.id/pay/TRX261004A8F3K2M9QX/qr.svg",
"qr_png_url": "https://kodebayar.id/pay/TRX261004A8F3K2M9QX/qr.png",
"qr_string": "00020101021226...",
"expires_at": "2026-10-04T15:30:00+07:00",
"paid_at": null,
"is_sandbox": false
}
}
Cek status
{id} boleh berisi trx_id atau reference_id. Bentuk jawabannya sama seperti saat membuat transaksi. Pakai sebagai cadangan bila webhook terlambat; jangan dipanggil terus-menerus untuk tiap transaksi.
Daftar transaksi
Mengembalikan transaksi terbaru lebih dulu. status, page, dan per_page (maks 100) opsional. Informasi halaman ada di meta.
Endpoint ini juga cara paling hemat untuk memantau banyak pesanan sekaligus tanpa webhook: satu panggilan mengembalikan semua yang baru lunas. Lihat Tahu kapan lunas.
Batalkan transaksi
Hanya untuk transaksi berstatus pending. Setelah dibatalkan, QR tidak bisa dibayar lagi dan webhook berstatus cancelled dikirim.
Hitung biaya
Biaya saat ini 0,7% + Rp 200 per transaksi. Bagian persen dibulatkan ke atas ke Rupiah terdekat. Siapa yang menanggung biaya diatur per merchant di dashboard.
{
"status": "success",
"data": {
"amount": 100000,
"fee_flat": 200,
"fee_percent": 0.7,
"fee": 900,
"fee_bearer": "customer",
"total_amount": 100900,
"net_amount": 100000
}
}
Webhook
Isi URL webhook di halaman merchant. Setiap kali status transaksi berubah menjadi paid, expired, cancelled, atau failed, kami mengirim HTTP POST berisi JSON ke URL tersebut.
{
"event": "payment.status_updated",
"trx_id": "TRX261004A8F3K2M9QX",
"reference_id": "INV-1042",
"channel": "qris",
"status": "paid",
"amount": 100000,
"fee": 900,
"fee_bearer": "customer",
"total_amount": 100900,
"net_amount": 100000,
"customer_name": "Budi Santoso",
"customer_email": null,
"paid_at": "2026-10-04T14:35:12+07:00",
"created_at": "2026-10-04T14:30:00+07:00",
"is_sandbox": false
}
Verifikasi tanda tangan
Setiap webhook membawa header X-Digitals-Signature dan X-Digitals-Timestamp. Hitung ulang tanda tangan dengan webhook secret merchant, lalu tolak permintaan yang tidak cocok.
signature = HMAC-SHA256(timestamp + "." + raw_body, webhook_secret)
$payload = file_get_contents('php://input'); $timestamp = $_SERVER['HTTP_X_DIGITALS_TIMESTAMP'] ?? ''; $signature = $_SERVER['HTTP_X_DIGITALS_SIGNATURE'] ?? ''; $expected = hash_hmac('sha256', $timestamp . '.' . $payload, $webhookSecret); if (! hash_equals($expected, $signature)) { http_response_code(401); exit; } $data = json_decode($payload, true); if ($data['status'] === 'paid') { // Tandai pesanan $data['reference_id'] lunas (cek dulu supaya tidak diproses dua kali). } http_response_code(200);
const crypto = require('crypto'); app.post('/webhook/digitals', express.raw({ type: 'application/json' }), (req, res) => { const payload = req.body.toString(); const expected = crypto .createHmac('sha256', process.env.DIGITALS_WEBHOOK_SECRET) .update(`${req.headers['x-digitals-timestamp']}.${payload}`) .digest('hex'); const given = String(req.headers['x-digitals-signature'] || ''); if (given.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected))) { return res.sendStatus(401); } const data = JSON.parse(payload); if (data.status === 'paid') { // Tandai pesanan data.reference_id lunas. } res.sendStatus(200); });
Aturan pengiriman
Binance Pay
Pembayaran USDT untuk pembeli luar negeri. Dananya masuk langsung ke akun Binance Anda: tanpa saldo, masa tahan, atau penarikan.
Persiapan
Binance Pay adalah layanan terpisah dari QRIS: punya kunci sendiri (bk_live_ dan bk_test_) dan endpoint sendiri di bawah /binance. Kunci merchant (sk_) tidak berlaku di sini, dan sebaliknya.
Aktifkan langganan
Binance Pay adalah fitur berlangganan. Pilih paket 1, 3, atau 6 bulan di menu Binance Pay dan bayar lewat QRIS.
Hubungkan akun Binance
Di halaman Akun Binance, isi API Key khusus baca dari Binance. Kunci itu hanya dipakai membaca USDT yang masuk.
Ambil kunci API
Di halaman Kunci API, salin kunci bk_ dan webhook secret, lalu isi URL webhook Anda.
Buat tagihan USDT
Parameter lainnya sama dengan transaksi QRIS (customer_name, description, return_url, expires_in). Harganya dikirim dengan salah satu dari dua cara:
curl -X POST https://kodebayar.id/api/v1/binance/transactions \ -H "Authorization: Bearer bk_live_..." \ -H "Content-Type: application/json" \ -d '{"amount": 150000, "reference_id": "INV-1042"}'
{
"status": "success",
"data": {
"trx_id": "TRX261004A8F3K2M9QX",
"reference_id": "INV-1042",
"channel": "binance",
"status": "pending",
"currency": "USDT",
"amount": 150000,
"exchange_rate": 17867,
"amount_usdt": 8.3954,
"fee_usdt": 0,
"pay_amount_usdt": 8.3955,
"binance_pay_id": "123456789",
"pay_url": "https://kodebayar.id/pay/TRX261004A8F3K2M9QX",
"expires_at": "2026-10-04T15:30:00+07:00"
}
}
Bila kurs sedang tidak tersedia, pembuatan tagihan dari amount dijawab 503; coba lagi atau kirim amount_usdt.
Semua endpoint
Semuanya memakai kunci bk_. {id} boleh berisi trx_id atau reference_id.
Cek pembayaran saat itu juga
curl -X POST https://kodebayar.id/api/v1/binance/payment/check \ -H "Authorization: Bearer bk_live_..." \ -H "Content-Type: application/json" \ -d '{"trx_id": "INV-1042", "order_id": "M_P_71505104267788288"}'
order_id opsional: ID transaksi Binance yang diberikan pembeli. Bila diisi, pembayaran itu dipasangkan langsung ke tagihan tersebut, asalkan memang USDT yang masuk ke akun Anda, nominalnya persis sama dengan pay_amount_usdt, dan belum dipakai melunasi tagihan lain. Jawabannya berbentuk sama seperti cek status; periksa status.
Dibatasi 10 panggilan per menit. history dan payment/check membaca langsung dari Binance. Panggil hanya saat pembeli menekan tombol "Saya sudah bayar", bukan otomatis berulang.
Webhook dan catatan
Pembayaran biasanya terdeteksi 15 sampai 30 detik setelah pembeli mengirim. Webhook payment.status_updated lalu dikirim ke URL webhook yang diisi di halaman Kunci API, ditandatangani dengan webhook secret dari halaman yang sama. Cara memeriksa tanda tangannya sama dengan webhook QRIS.
{
"event": "payment.status_updated",
"trx_id": "TRX261004A8F3K2M9QX",
"reference_id": "INV-1042",
"channel": "binance",
"status": "paid",
"currency": "USDT",
"amount": 150000,
"exchange_rate": 17867,
"amount_usdt": 8.3954,
"fee_usdt": 0,
"pay_amount_usdt": 8.3955,
"binance_transaction_id": "M_P_71505104267788288",
"paid_at": "2026-10-04T14:35:12+07:00",
"is_sandbox": false
}
Yang perlu diperhatikan
Bot Telegram
Pasang QRIS dan Binance Pay sebagai pilihan bayar di bot Anda. API-nya sama; yang berbeda hanya cara menampilkannya.
Bedanya dengan website
Bot adalah program di server Anda, jadi ia memanggil API yang sama dengan kunci yang sama. Tidak ada endpoint khusus Telegram. Perbedaannya ada di tampilan dan di cara bot tahu pembayaran sudah masuk:
Menampilkan QRIS
WebsiteArahkan ke pay_url, atau pasang gambar QR di halaman.
BotKirim gambar QR sebagai foto di chat, memakai qr_png_url.
Menampilkan Binance Pay
WebsiteHalaman bayar dengan nominal dan Binance ID.
BotPesan teks berisi Binance ID dan nominal yang tersalin dengan sekali ketuk.
Setelah lunas
WebsitePembeli diarahkan ke return_url.
BotTidak ada pengalihan. Bot mengirim pesan konfirmasi dan menghapus foto QR.
Tahu sudah lunas
WebsiteWebhook ke server website.
BotWebhook bila bot punya alamat publik, atau cek daftar transaksi secara berkala.
Yang perlu disiapkan
Simpan token dan kunci di variabel lingkungan server, bukan di dalam kode. Contoh di bawah membacanya dari BOT_TOKEN, QRIS_API_KEY, dan BINANCE_API_KEY.
QRIS di bot
Buat transaksi seperti biasa, lalu kirim qr_png_url sebagai foto. Telegram mengambil gambarnya sendiri dari alamat itu, jadi bot tidak perlu membuat atau mengunggah berkas gambar. Pembeli tidak perlu menekan tombol apa pun setelah membayar: bot yang mengabari.
Pembeli memilih bayar QRIS
Bot memanggil POST /transactions dan mencatat reference_id bersama ID chat.
Bot mengirim foto QR
Sertakan total_amount dan batas waktu di keterangan foto.
Pembeli scan dan membayar
Dari aplikasi bank atau dompet digital mana pun.
Bot mengonfirmasi
Saat status menjadi paid, hapus foto QR dan kirim pesan atau produknya.
Contoh lengkap
Bot kecil yang berjalan apa adanya: perintah /beli membuat transaksi Rp 25.000, mengirim QR, lalu mengabari saat lunas atau kedaluwarsa. Contoh ini memakai cek berkala, sehingga bisa dijalankan tanpa alamat publik.
// npm install telegraf (butuh Node.js 18 ke atas) const { Telegraf } = require('telegraf'); const API = 'https://kodebayar.id/api/v1'; const KEY = process.env.QRIS_API_KEY; // sk_live_... atau sk_test_... const bot = new Telegraf(process.env.BOT_TOKEN); // Pesanan yang menunggu pembayaran: reference_id -> { chatId, messageId, expiresAt }. // Di bot sungguhan simpan di database, supaya tidak hilang saat bot dinyalakan ulang. const waiting = new Map(); async function api(method, path, body) { const res = await fetch(API + path, { method, headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' }, body: body ? JSON.stringify(body) : undefined, }); const json = await res.json(); if (!res.ok) throw new Error(json.message || `HTTP ${res.status}`); return json.data; } // 1. Pembeli memesan: buat transaksi, lalu kirim QR sebagai foto. bot.command('beli', async (ctx) => { const reference = `TG-${ctx.chat.id}-${Date.now()}`; try { const trx = await api('POST', '/transactions', { amount: 25000, reference_id: reference, expires_in: 15 }); const photo = await ctx.replyWithPhoto(trx.qr_png_url, { caption: `Scan QRIS ini untuk membayar Rp ${trx.total_amount.toLocaleString('id-ID')}.\nBerlaku 15 menit.`, }); waiting.set(reference, { chatId: ctx.chat.id, messageId: photo.message_id, expiresAt: Date.parse(trx.expires_at) }); } catch (err) { console.error(err); await ctx.reply('Pembayaran sedang tidak tersedia. Coba lagi sebentar.'); } }); // 2. Pesanan selesai: hapus foto QR dan kabari pembeli. async function selesai(reference, pesan) { const order = waiting.get(reference); if (!order) return; waiting.delete(reference); // hapus dulu, supaya tidak diproses dua kali await bot.telegram.deleteMessage(order.chatId, order.messageId).catch(() => {}); await bot.telegram.sendMessage(order.chatId, pesan); } // 3. Satu panggilan tiap 5 detik untuk SEMUA pesanan, bukan satu panggilan per pesanan. setInterval(async () => { if (waiting.size === 0) return; try { const paid = await api('GET', '/transactions?status=paid&per_page=50'); for (const trx of paid) { if (waiting.has(trx.reference_id)) { await selesai(trx.reference_id, 'Pembayaran diterima. Pesanan Anda diproses.'); // Kirim produk ke pembeli di sini. } } for (const [reference, order] of waiting) { if (Date.now() < order.expiresAt) continue; // Cek sekali lagi sebelum menyatakan kedaluwarsa: pembeli bisa saja membayar di detik terakhir. const last = await api('GET', `/transactions/${reference}`); await selesai(reference, last.status === 'paid' ? 'Pembayaran diterima. Pesanan Anda diproses.' : 'Waktu pembayaran habis. Ketik /beli untuk membuat QR baru.'); } } catch (err) { console.error(err); } }, 5000); bot.launch();
# pip install "python-telegram-bot[job-queue]" httpx import os import time from datetime import datetime import httpx from telegram import Update from telegram.ext import Application, CommandHandler, ContextTypes API = "https://kodebayar.id/api/v1" KEY = os.environ["QRIS_API_KEY"] # sk_live_... atau sk_test_... client = httpx.AsyncClient(headers={"Authorization": f"Bearer {KEY}"}, timeout=20) # Pesanan yang menunggu pembayaran: reference_id -> {chat_id, message_id, expires_at}. # Di bot sungguhan simpan di database, supaya tidak hilang saat bot dinyalakan ulang. waiting = {} async def api(method, path, body=None): res = await client.request(method, API + path, json=body) data = res.json() if res.is_error: raise RuntimeError(data.get("message", f"HTTP {res.status_code}")) return data["data"] # 1. Pembeli memesan: buat transaksi, lalu kirim QR sebagai foto. async def beli(update: Update, context: ContextTypes.DEFAULT_TYPE): chat_id = update.effective_chat.id reference = f"TG-{chat_id}-{int(time.time() * 1000)}" try: trx = await api("POST", "/transactions", {"amount": 25000, "reference_id": reference, "expires_in": 15}) except Exception as err: print(err) await update.message.reply_text("Pembayaran sedang tidak tersedia. Coba lagi sebentar.") return total = f"{trx['total_amount']:,}".replace(",", ".") photo = await update.message.reply_photo( trx["qr_png_url"], caption=f"Scan QRIS ini untuk membayar Rp {total}.\nBerlaku 15 menit." ) waiting[reference] = { "chat_id": chat_id, "message_id": photo.message_id, "expires_at": datetime.fromisoformat(trx["expires_at"]).timestamp(), } # 2. Pesanan selesai: hapus foto QR dan kabari pembeli. async def selesai(bot, reference, pesan): order = waiting.pop(reference, None) # diambil dulu, supaya tidak diproses dua kali if not order: return try: await bot.delete_message(order["chat_id"], order["message_id"]) except Exception: pass await bot.send_message(order["chat_id"], pesan) # 3. Satu panggilan tiap 5 detik untuk SEMUA pesanan, bukan satu panggilan per pesanan. async def cek(context: ContextTypes.DEFAULT_TYPE): if not waiting: return try: for trx in await api("GET", "/transactions?status=paid&per_page=50"): if trx["reference_id"] in waiting: await selesai(context.bot, trx["reference_id"], "Pembayaran diterima. Pesanan Anda diproses.") # Kirim produk ke pembeli di sini. for reference, order in list(waiting.items()): if time.time() < order["expires_at"]: continue # Cek sekali lagi sebelum menyatakan kedaluwarsa: pembeli bisa saja membayar di detik terakhir. last = await api("GET", f"/transactions/{reference}") await selesai( context.bot, reference, "Pembayaran diterima. Pesanan Anda diproses." if last["status"] == "paid" else "Waktu pembayaran habis. Ketik /beli untuk membuat QR baru.", ) except Exception as err: print(err) app = Application.builder().token(os.environ["BOT_TOKEN"]).build() app.add_handler(CommandHandler("beli", beli)) app.job_queue.run_repeating(cek, interval=5) app.run_polling()
Binance Pay di bot
Tagihan USDT tidak punya QR. Kirim binance_pay_id dan pay_amount_usdt sebagai teks di dalam tag <code>, supaya pembeli bisa menyalinnya dengan sekali ketuk lalu menempelkannya di aplikasi Binance.
// Lanjutan contoh QRIS di atas. Binance memakai kunci dan alamat sendiri. const { Markup } = require('telegraf'); const BINANCE_KEY = process.env.BINANCE_API_KEY; // bk_live_... atau bk_test_... async function binance(method, path, body) { const res = await fetch(API + '/binance' + path, { method, headers: { Authorization: `Bearer ${BINANCE_KEY}`, 'Content-Type': 'application/json' }, body: body ? JSON.stringify(body) : undefined, }); const json = await res.json(); if (!res.ok) throw new Error(json.message || `HTTP ${res.status}`); return json.data; } // 1. Kirim Binance ID dan nominal sebagai teks. Isi tag <code> tersalin dengan sekali ketuk. bot.command('usdt', async (ctx) => { const reference = `TG-${ctx.chat.id}-${Date.now()}`; try { const bill = await binance('POST', '/transactions', { amount: 25000, reference_id: reference, expires_in: 30 }); await ctx.replyWithHTML( 'Kirim lewat <b>Binance Pay</b>:\n\n' + `Binance ID: <code>${bill.binance_pay_id}</code>\n` + `Nominal: <code>${bill.pay_amount_usdt}</code> USDT\n\n` + 'Ketuk angkanya untuk menyalin. Nominal harus persis, jangan dibulatkan.', Markup.inlineKeyboard([Markup.button.callback('Saya sudah bayar', `cek:${reference}`)]), ); } catch (err) { console.error(err); await ctx.reply('Pembayaran USDT sedang tidak tersedia. Coba lagi sebentar.'); } }); // 2. Tombol "Saya sudah bayar": minta pengecekan saat itu juga. bot.action(/^cek:(.+)$/, async (ctx) => { try { const bill = await binance('POST', '/payment/check', { trx_id: ctx.match[1] }); if (bill.status === 'paid') { await ctx.answerCbQuery(); await ctx.editMessageText('Pembayaran USDT diterima. Pesanan Anda diproses.'); // Kirim produk ke pembeli di sini (pastikan hanya sekali per reference_id). return; } await ctx.answerCbQuery('Belum terlihat. Tunggu 15 sampai 30 detik, lalu coba lagi.', { show_alert: true }); } catch (err) { console.error(err); await ctx.answerCbQuery('Belum bisa dicek. Coba lagi sebentar.', { show_alert: true }); } });
# Lanjutan contoh QRIS di atas. Binance memakai kunci dan alamat sendiri. from telegram import InlineKeyboardButton, InlineKeyboardMarkup from telegram.ext import CallbackQueryHandler BINANCE_KEY = os.environ["BINANCE_API_KEY"] # bk_live_... atau bk_test_... async def binance(method, path, body=None): res = await client.request( method, API + "/binance" + path, json=body, headers={"Authorization": f"Bearer {BINANCE_KEY}"} ) data = res.json() if res.is_error: raise RuntimeError(data.get("message", f"HTTP {res.status_code}")) return data["data"] # 1. Kirim Binance ID dan nominal sebagai teks. Isi tag <code> tersalin dengan sekali ketuk. async def usdt(update: Update, context: ContextTypes.DEFAULT_TYPE): reference = f"TG-{update.effective_chat.id}-{int(time.time() * 1000)}" try: bill = await binance("POST", "/transactions", {"amount": 25000, "reference_id": reference, "expires_in": 30}) except Exception as err: print(err) await update.message.reply_text("Pembayaran USDT sedang tidak tersedia. Coba lagi sebentar.") return await update.message.reply_html( "Kirim lewat <b>Binance Pay</b>:\n\n" f"Binance ID: <code>{bill['binance_pay_id']}</code>\n" f"Nominal: <code>{bill['pay_amount_usdt']}</code> USDT\n\n" "Ketuk angkanya untuk menyalin. Nominal harus persis, jangan dibulatkan.", reply_markup=InlineKeyboardMarkup( [[InlineKeyboardButton("Saya sudah bayar", callback_data=f"cek:{reference}")]] ), ) # 2. Tombol "Saya sudah bayar": minta pengecekan saat itu juga. async def cek_usdt(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query try: bill = await binance("POST", "/payment/check", {"trx_id": query.data.split(":", 1)[1]}) except Exception as err: print(err) await query.answer("Belum bisa dicek. Coba lagi sebentar.", show_alert=True) return if bill["status"] == "paid": await query.answer() await query.edit_message_text("Pembayaran USDT diterima. Pesanan Anda diproses.") # Kirim produk ke pembeli di sini (pastikan hanya sekali per reference_id). else: await query.answer("Belum terlihat. Tunggu 15 sampai 30 detik, lalu coba lagi.", show_alert=True) # Daftarkan sebelum app.run_polling(): app.add_handler(CommandHandler("usdt", usdt)) app.add_handler(CallbackQueryHandler(cek_usdt, pattern=r"^cek:"))
Untuk konfirmasi otomatis tanpa tombol, pakai cara yang sama dengan QRIS di bagian berikut: webhook, atau cek GET /binance/transactions?status=paid secara berkala.
Tahu kapan lunas
Bot tidak perlu memberi tahu kami bahwa pembeli sudah membayar. Status transaksi berubah sendiri di sistem kami; bot hanya perlu mengetahuinya. Ada dua cara:
Webhook
Kami yang mengabari bot begitu status berubah. Tanpa jeda dan tanpa memakai jatah panggilan. Butuh alamat HTTPS publik, misalnya bot di VPS dengan domain.
Cek berkala
Bot bertanya tiap beberapa detik. Cocok untuk bot yang berjalan di komputer sendiri atau hosting tanpa domain. Ada jeda beberapa detik.
Cara 1: webhook
Isi URL webhook di halaman merchant (QRIS) atau di halaman Kunci API (Binance Pay). Saat webhook tiba, periksa tanda tangannya, cari pesanan dari reference_id, lalu kirim pesan ke chat pembelinya.
// npm install express Dijalankan di proses yang sama dengan bot.
const crypto = require('crypto');
const express = require('express');
const app = express();
app.post('/webhook/qris', express.raw({ type: 'application/json' }), async (req, res) => {
const payload = req.body.toString();
const expected = crypto
.createHmac('sha256', process.env.QRIS_WEBHOOK_SECRET)
.update(`${req.headers['x-digitals-timestamp']}.${payload}`)
.digest('hex');
const given = String(req.headers['x-digitals-signature'] || '');
if (given.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected))) {
return res.sendStatus(401);
}
res.sendStatus(200); // jawab dulu, baru kabari pembeli
const data = JSON.parse(payload);
if (data.status === 'paid') {
await selesai(data.reference_id, 'Pembayaran diterima. Pesanan Anda diproses.');
} else if (data.status === 'expired') {
await selesai(data.reference_id, 'Waktu pembayaran habis. Ketik /beli untuk membuat QR baru.');
}
});
app.listen(3000);
# pip install flask requests Layanan kecil terpisah dari proses bot. import hashlib import hmac import os import requests from flask import Flask, request app = Flask(__name__) SECRET = os.environ["QRIS_WEBHOOK_SECRET"].encode() TELEGRAM = f"https://api.telegram.org/bot{os.environ['BOT_TOKEN']}" @app.post("/webhook/qris") def webhook(): payload = request.get_data() timestamp = request.headers.get("X-Digitals-Timestamp", "").encode() expected = hmac.new(SECRET, timestamp + b"." + payload, hashlib.sha256).hexdigest() if not hmac.compare_digest(expected, request.headers.get("X-Digitals-Signature", "")): return "", 401 data = request.get_json() if data["status"] == "paid": # Ambil chat_id dari pesanan yang Anda simpan saat membuat transaksi, # dan pastikan pesanan ini belum pernah diproses. chat_id = cari_chat_id(data["reference_id"]) requests.post(f"{TELEGRAM}/sendMessage", json={"chat_id": chat_id, "text": "Pembayaran diterima. Pesanan Anda diproses."}) return "", 200
Dengan webhook, bagian cek berkala (setInterval atau run_repeating) di contoh lengkap tidak diperlukan lagi.
Cara 2: cek berkala
Jangan mengecek tiap transaksi satu per satu. Batas API adalah 60 panggilan per menit per kunci, sehingga mengecek tiap pesanan setiap 5 detik sudah mentok saat ada 5 pembeli bersamaan.
Yang perlu diperhatikan
Kode galat
Jawaban gagal selalu berbentuk { "status": "error", "message": "..." }. Tampilkan pesan umum ke pembeli dan catat message aslinya di log Anda.
Siap mencoba?
Kunci uji langsung aktif setelah mendaftar. Tidak perlu menunggu persetujuan untuk mulai menulis kode.