Cara menerima notifikasi pembayaran di sistem Anda sendiri, dan cara memakai API QRIS & pesanan.
HP Anda mengirim POST ke URL webhook Anda setiap ada notifikasi yang cocok. Nominal diekstrak di HP, dan kiriman webhook berangkat langsung dari HP ke URL Anda — tidak lewat server kami.
POST https://webhook-anda.com/notivgo
Content-Type: application/json; charset=utf-8
X-Device-Id: dev_xxx
X-Timestamp: 1723800000
X-Nonce: 3f2a…
X-Signature: sha256=…
X-NotivGo-Event: notification.forwarded
X-NotivGo-Schema: 2
X-NotivGo-Attempt: 1
Idempotency-Key: evt_01H…
{
"event_id": "evt_01H…",
"device_id": "dev_xxx",
"source": "notivgo",
"package": "id.dana",
"app_name": "DANA",
"post_time": 1723799995000,
"captured_at": 1723799995120,
"amount": 150000,
"direction": "in",
"currency": "IDR",
"title": "Dana masuk",
"text": "Anda menerima Rp150.000 dari B***",
"big_text": "",
"sub_text": "",
"summary_text": "",
"ticker_text": "",
"channel_id": "…",
"event": "notification.forwarded",
"schema_version": 2,
"attempt": 1
}
Selain webhook, HP juga bisa mengirim ke Telegram (pesan ke chat/grup Anda) dan Google Sheets (baris baru lewat Apps Script). Atur di portal → Tujuan kiriman.
| Field | Arti |
|---|---|
event_id | ID unik notifikasi. Sama di setiap percobaan ulang — pakai untuk menolak duplikat. |
package, app_name | Aplikasi sumber notifikasi (mis. id.dana, DANA). |
post_time, captured_at | Waktu notifikasi muncul & waktu dibaca HP, dalam milidetik epoch. |
amount | Nominal rupiah (angka bulat) hasil baca di HP, atau null bila tidak terbaca. |
direction | "in" (uang masuk), "out" (uang keluar), atau null (tidak yakin). Hanya "in" yang aman dianggap pembayaran. |
currency | "IDR" bila nominal terbaca, selain itu null. |
title, text, big_text, … | Teks notifikasi apa adanya (disamarkan bila Anda menyalakan penyamaran di HP). |
event | notification.forwarded, atau webhook.test untuk kiriman contoh. |
attempt | Percobaan ke berapa (1, 2, …). |
Keputusan akhir "sudah dibayar" tetap di tangan Anda: cocokkan dengan mutasi resmi sebelum menyerahkan barang bernilai besar.
Jangan percaya isi request sebelum tanda tangannya cocok. Basis tanda tangan
adalah timestamp.nonce.rawbody — pakai raw body, bukan hasil
parse JSON, karena spasi sekecil apa pun mengubah hasilnya.
<?php
$raw = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_TIMESTAMP'];
$nonce = $_SERVER['HTTP_X_NONCE'];
$sig = $_SERVER['HTTP_X_SIGNATURE'];
$want = 'sha256=' . hash_hmac('sha256', "$ts.$nonce.$raw", $WEBHOOK_SECRET);
if (!hash_equals($want, $sig)) { http_response_code(401); exit; }
// Tolak yang terlalu tua (anti replay) — balas clock_skew supaya HP mengoreksi jamnya.
if (abs(time() - (int)$ts) > 300) {
http_response_code(401);
header('Content-Type: application/json');
echo json_encode(['error' => 'clock_skew', 'server_time' => time()]);
exit;
}
// Lalu abaikan event_id yang sudah pernah diproses.
// Node.js (Express, body mentah)
const crypto = require('crypto');
app.post('/notivgo', express.raw({type: '*/*'}), (req, res) => {
const ts = req.get('X-Timestamp'), nonce = req.get('X-Nonce');
const want = 'sha256=' + crypto.createHmac('sha256', SECRET)
.update(ts + '.' + nonce + '.' + req.body).digest('hex');
const got = req.get('X-Signature') || '';
if (got.length !== want.length ||
!crypto.timingSafeEqual(Buffer.from(got), Buffer.from(want))) return res.sendStatus(401);
const ev = JSON.parse(req.body);
// … simpan ev.event_id, abaikan bila sudah pernah …
res.sendStatus(200);
});
Secret webhook ada di portal → Webhook. Bisa dirotasi kapan saja; HP mengikuti otomatis lewat heartbeat (≤15 menit), tanpa pairing ulang.
| Balasan server Anda | Yang dilakukan HP |
|---|---|
2xx | Terkirim — selesai. |
408, 429, 5xx, jaringan putus/timeout | Dicoba ulang otomatis dengan jeda bertambah, sampai batas percobaan. |
401 berisi {"error":"clock_skew","server_time":…} | HP mengoreksi selisih jamnya lalu mencoba lagi. |
401/403 lainnya | Berhenti — biasanya secret salah. Periksa secret di portal. |
3xx | Tidak diikuti (redirect bisa membuang isi POST) — dianggap gagal. Pakai URL akhir. |
4xx lainnya | Gagal tanpa diulang. |
Jaringan seluler bisa membuat satu event terkirim lebih dari sekali.
Simpan event_id dan abaikan yang sudah pernah diproses —
inilah pengaman agar pesanan tidak terhitung dua kali.
Buat QR bernominal dari QRIS statis Anda lewat API. Dibayar per QR yang dibuat. API key dibuat di portal → Developer.
POST /v1/generate
Authorization: Bearer ngk_…
Content-Type: application/json
{"order_ref":"INV-001","amount":"150000","format":"payload"}
| Field kiriman | Arti |
|---|---|
order_ref | Nomor pesanan Anda, unik per akun. Mengirim ulang ref yang sama mengembalikan QR yang sama tanpa menagih dua kali (HTTP 200). |
amount | Nominal rupiah (angka atau teks angka). |
qris_id | Opsional: QRIS mana yang dipakai bila akun punya lebih dari satu. Kosong = QRIS pertama. |
format | "payload" (bawaan) atau "png" (ikut mengirim gambar QR base64). |
HTTP 201
{"generate_id":"gen_…","order_ref":"INV-001","qris_payload":"000201…6304ABCD","metered":true}
metered:false + HTTP 200 berarti ulang-kirim ref yang sama (tidak ditagih lagi).
Daftarkan pesanan beserta nominalnya; saat HP melaporkan uang masuk bernominal sama dalam jendela waktunya, server menandai pesanan itu paid. Nominal kembar pada saat bersamaan tidak ditebak — ditandai untuk Anda tinjau di portal.
POST /v1/orders
Authorization: Bearer ngk_…
{"order_ref":"INV-001","amount":150000,"note":"Meja 4","expires_in":1800}
→ 201 {"order_ref":"INV-001","amount":150000,"status":"open","expires_at":1723801800}
GET /v1/orders/INV-001 → {"order_ref":…,"status":"open|paid|expired|canceled","paid_at":…}
POST /v1/orders/INV-001/cancel → {"order_ref":"INV-001","status":"canceled"}
expires_in dalam detik (0 = tanpa kedaluwarsa, maks 90 hari). Mengirim ulang
order_ref yang sama dengan isi sama → 200 "duplicate":true; dengan nominal/catatan berbeda → 409.
Batas 600 permintaan per menit per akun.
Semua galat berbentuk {"error":"kode","message":"penjelasan"}.
| HTTP | error | Artinya |
|---|---|---|
| 400 | bad_json, bad_request, unsupported_currency, bad_format | Isi permintaan tidak sah — lihat message. |
| 401 | unauthorized | API key salah atau sudah dicabut. |
| 402 | credit_limit_reached | Batas tagihan tercapai — lunasi tagihan di portal, QR baru terbuka lagi. |
| 403 | account_inactive | Akun belum aktif (verifikasi email) atau ditangguhkan. |
| 404 | qris_not_found, not_found | QRIS/pesanan tidak ada di akun ini. |
| 409 | order_ref_conflict, webhook_required, not_cancelable | Nomor pesanan sudah dipakai dengan isi lain · akun belum punya tujuan kiriman · pesanan bukan berstatus open. |
| 429 | rate_limited | Terlalu cepat — tunggu sebentar (lihat header Retry-After). |
NotivGo adalah alat bantu, bukan payment gateway: kami tidak pernah memegang uang Anda. ← kembali ke portal