Doc Updated — 8 Oktober 2026, 17:20 WIB
Versi inilah yang berlaku. Jangan mengacu ke salinan atau backup dokumentasi lama.
Perubahan terakhir: kanal BRI Merchant (server-side, QR dinamis ±5 menit, tanpa kode unik), pemilihan kanal via channel/default merchant, pengalihan otomatis ke GoBiz saat kanal BRI bermasalah, serta normalisasi payment_method/issuer untuk semua kanal.

WebQRIS API Documentation

Notifikasi QRIS — REST API v3.6
Base URL: https://webqris.com
Authentication: Bearer Token via header Authorization: Bearer YOUR_API_TOKEN
WebSocket: wss://webqris.com/ws (dashboard, session aktif) | wss://webqris.com/ws/user (user, session aktif) | wss://webqris.com/ws/merchant?token=API_TOKEN (merchant)
GoBiz Detector BRI Merchant Persiapan Integrasi Ringkasan Create QRIS Check Status Webhook Outbound Webhook APK Callback Notify WebSocket Error Alur
INFO GoBiz Detector — metode deteksi utama

WebQRIS membaca mutasi QRIS langsung dari portal GoPay Merchant / GoBiz kamu. Tidak butuh HP menyala dan tidak butuh APK. Invoice tetap dibuat lewat POST /api/payments/qris/create seperti biasa — yang berbeda hanya siapa yang menandai invoice menjadi paid.

Urutannya empat langkah: (1) siapkan akun GoBiz, (2) daftarkan akun itu di halaman GoBiz Detector, (3) hubungkan merchant, (4) aktifkan polling. Kalau keempat langkah ini tercentang, akun siap dipakai.
Langkah 1 — Siapkan akun GoBiz (di portal GoBiz, bukan di WebQRIS)
  1. Kalau belum punya akun GoFood Merchant, daftar dulu di https://portal.gofoodmerchant.co.id/auth/registration/corporate. Kamu akan diminta mengisi email dan password yang diinginkan.
  2. Bisa juga masuk lewat nomor HP di https://portal.gofoodmerchant.co.id/auth/login. OTP akan dikirim ke nomor yang sudah terdaftar.
  3. ⚠️ Password dari halaman pendaftaran biasanya belum bisa dipakai untuk login email. Ini perilaku portal GoBiz, bukan kesalahan WebQRIS — jadi wajar kalau login email gagal di percobaan pertama.
  4. Buka https://portal.gofoodmerchant.co.id/auth/login/email, masukkan email dulu. Sebelum mengisi password, klik RESET PASSWORD / ATUR ULANG PASSWORD yang ada di bawah kolom password.
  5. Setelah password diatur ulang, login email baru bisa berhasil memakai password baru tersebut.
  6. Email dan password hasil atur ulang itulah yang dipakai di menu Tambah Akun GoBiz di WebQRIS.
Ringkasnya: daftar → login email → Atur Ulang Password → baru password itu bisa dipakai. Kalau langkah atur ulang dilewatkan, WebQRIS akan melaporkan gagal login — dan itu bukan salah konfigurasi WebQRIS.
Langkah 2 — Daftarkan akun itu di WebQRIS

Buka dashboard → menu GoBiz Detector → bagian Tambah akun GoBiz. Isiannya:

OwnerPemilik akun GoBiz. Satu akun GoBiz dimiliki satu owner dan kepemilikannya tidak bisa dipindahkan — kalau salah owner, buat source baru.
Nama akun/sourceLabel bebas untuk kamu sendiri, misalnya “GoBiz Toko A”.
Email GoBizEmail akun GoBiz yang sudah melewati langkah 1. Harus alamat email lengkap, bukan username atau nomor HP.
Merchant ID GoBizID merchant / NMID milik akun tersebut. Salah ID di sini membuat polling tetap jalan tetapi transaksi tidak pernah terbaca.
PasswordPassword hasil Atur Ulang Password. Boleh dikosongkan kalau memilih jalur OTP atau sudah meng-import token.
X-AppVersionDiisi sistem, tidak perlu diubah. Ikut meniru versi aplikasi web GoBiz yang dipakai membaca transaksi.
Lookback (menit)Seberapa jauh ke belakang transaksi dibaca setiap kali polling. Default 30 menit.
Tiga cara autentikasi (pilih salah satu)
PasswordPaling sederhana. Isi email + password, lalu klik Test Login. Token disimpan dan diperbarui otomatis.
OTPPakai kalau akun tidak memakai password. Klik Kirim OTP, cek email GoBiz, lalu masukkan kode OTP pada form.
Import token browserOpsi lanjutan. Tempel JSON token dari browser bila login biasa dan OTP sama-sama tidak bisa dipakai.
Langkah 3 — Hubungkan merchant, lalu aktifkan polling
  1. Di kartu akun GoBiz, hubungkan minimal satu merchant WebQRIS milik owner tersebut.
  2. Jalankan Test Transaksi sampai transaksi GoBiz terbaca. Kalau kosong, kemungkinan Merchant ID atau jendela lookback belum tepat.
  3. Baru setelah itu set status Active. Tombol Active sengaja terkunci sampai login dan merchant siap.
  4. Invoice baru tetap dibuat lewat API WebQRIS yang sama; detector menandai invoice paid begitu settlement GoBiz cocok.
Langkah 4 — Pastikan tandanya sudah benar

Akun yang sudah benar menampilkan badge Sehat dan active, dengan keempat langkah tercentang:

1. KonfigurasiEmail dan Merchant ID GoBiz sudah terisi.
2. AutentikasiLogin berhasil dan token tersimpan.
3. Hubungkan merchantMinimal satu merchant WebQRIS terhubung.
4. Aktifkan pollingPolling berjalan dan tercatat “Polling berhasil terakhir …”.
Kalau akun GoBiz-mu sudah terverifikasi: tidak ada langkah tambahan di sisi GoBiz. Cukup hubungkan akun itu ke merchant milikmu di halaman GoBiz Detector, lalu aktifkan polling.
Kalau ada masalah
GoBiz HTTP 403Akses ditolak. Biasanya password belum di-atur ulang (lihat langkah 1), atau akun belum punya akses merchant.
GoBiz HTTP 429Terlalu sering menembak server GoBiz. Tunggu saja; WebQRIS otomatis memperlambat dirinya sendiri.
GoBiz HTTP 400Permintaan tidak diterima. Cek kembali Merchant ID GoBiz.
Status transaksi ignoredTransaksi masuk tetapi tidak ada invoice terbuka dengan nominal sama. Kalau kamu yakin itu pembayaran pelanggan, pakai konfirmasi manual pada invoice yang bersangkutan.
Status transaksi conflictAda lebih dari satu invoice terbuka dengan nominal sama, jadi sistem tidak menebak. Selesaikan lewat panel rekonsiliasi.
Yang berubah saat merchant memakai GoBiz detector
payment_methodMenjadi gobiz_api saat invoice paid dari kanal GoBiz.
issuerIssuer QRIS seperti DANA, OVO, atau AIRPAY SHOPEE disimpan di data internal GoBiz dan dipakai untuk notifikasi WhatsApp, misalnya QRIS DANA.
matchingInvoice dicocokkan memakai total_amount unik dalam scope GoBiz Source, lalu divalidasi terhadap waktu transaksi agar transaksi lama tidak menempel ke QRIS baru.
APK callback/api/webhook/payment dan /api/callback/notify akan mengembalikan ignored: true untuk merchant yang sudah memakai kanal deteksi aktif (GoBiz atau BRI Merchant).
webhook outboundTetap memakai event payment.paid dan signature HMAC yang sama seperti jalur APK/callback.
Contoh data paid GoBiz
Webhook yang diterima sistem Anda
{
  "event": "payment.paid",
  "data": {
    "invoice_id": "INV-MERCHANT-1710000000-abc123",
    "merchant_order_id": "ORDER-001",
    "amount": 25000,
    "unique_code": 42,
    "total_amount": 25042,
    "customer_name": "John Doe",
    "status": "paid",
    "payment_method": "gobiz_api",
    "issuer": "DANA",
    "sender_name": "DANA",
    "funding_source": "DANA",
    "paid_at": "2026-03-09T10:15:23.000Z"
  }
}
Di kanal GoBiz, unique_code berisi 1–36 dan invoice dicocokkan lewat total_amount unik. Di kanal BRI Merchant, unique_code selalu 0 karena pencocokannya memakai refnum QR.
Transparansi: integrasi ini membaca dashboard/portal merchant GoBiz. WebQRIS tetap bukan payment gateway dan tidak menampung dana. Untuk jalur resmi penuh berbasis order ID, merchant dapat memakai GoBiz Open API/Midtrans bila sudah memiliki akses resmi.
INFO BRI Merchant — kanal server-side kedua

Selain GoBiz, WebQRIS bisa menandai invoice paid dari akun BRI Merchant (brimerchant.bri.co.id) milik merchant. Sama seperti GoBiz: tidak butuh HP menyala dan tidak butuh APK — koneksi login dijalankan otomatis dari server WebQRIS.

Bedanya dengan GoBiz: QRIS BRI Merchant adalah QR dinamis dari acquirer yang berlaku ±5 menit, dan invoice kanal ini tanpa kode unik (unique_code = 0) karena pencocokannya memakai refnum dari webhook — bukan nominal unik.
Langkah 1 — Siapkan akun BRI Merchant (di portal BRI, bukan di WebQRIS)
  1. Login ke https://brimerchant.bri.co.id memakai nomor HP dan password akun merchant BRI Anda.
  2. Catat MPAN outlet (19 digit, berawalan 936) yang akan dipakai menerima pembayaran.
  3. Tidak perlu menyiapkan apa pun lagi di portal — setelah terhubung, WebQRIS yang menjaga sesinya.
Langkah 2 — Daftarkan koneksi di WebQRIS

Buka dashboard → menu BRI Merchant → bagian Tambah koneksi BRI Merchant. Isiannya:

Nama koneksiLabel bebas, misalnya “BRI Outlet Toko A”.
No HP loginNomor HP akun BRI Merchant. Satu nomor HP = satu koneksi.
PasswordPassword akun BRI Merchant. Dipakai untuk login otomatis dan login ulang saat sesi berakhir.
MPANMPAN outlet milik akun tersebut (19 digit, berawalan 936).
TIDOpsional.

Koneksi baru tersimpan NONAKTIF. Klik Login otomatis untuk menguji kredensial (tombol Cek koneksi menampilkan daftar outlet & MPAN akun), lalu hubungkan merchant, baru klik Aktifkan kanal.

⚠️ Satu akun BRI Merchant = satu pemegang sesi. Jangan login akun yang sama lewat browser portal, APK Android, atau koneksi kedua di WebQRIS selama kanal aktif — login baru akan menendang sesi lama sehingga pembayaran bisa terlewat. Kalau perlu inspeksi akun, hubungi admin agar koneksi dijeda dulu.
Langkah 3 — Hubungkan merchant & aktifkan

Di bagian Hubungkan koneksi ke merchant, pilih koneksi + merchant (boleh beberapa merchant sekaligus). Merchant yang sudah tertaut otomatis disembunyikan dari daftar. Setelah tertaut, klik Aktifkan kanal pada kartu koneksi.

Masa berlaku QR — ikuti acquirer, bukan setelan
Kanal BRI MerchantQR berlaku ±5 menit (299 detik) sesuai batas acquirer. WebQRIS menyesuaikan expired_at invoice supaya tidak menampilkan QR yang sudah mati. Setelan payment_expiry_minutes merchant hanya jadi batas atas (plafon), tidak bisa memperpanjang.
Kanal GoBizTidak dibatasi acquirer, jadi expired_at mengikuti setelan payment_expiry_minutes merchant (mis. 15 menit).
Konsekuensi praktis: pada kanal BRI Merchant, pelanggan punya ±5 menit untuk membayar. Setelah itu invoice expired dan QR harus dibuat ulang. Pembayaran yang tetap masuk setelah expired tetap diproses (late settlement) selama nominalnya cocok.
Memilih kanal pada saat membuat invoice

Invoice dibuat dengan endpoint yang sama, POST /api/payments/qris/create. Yang menentukan kanal:

body.channelOpsional. Isi "bri_merchant" atau "gobiz". Kalau dikirim, nilai ini selalu menang.
default merchantKalau channel tidak dikirim, dipakai kanal default merchant yang diatur admin di dashboard. Jadi aplikasi lama tetap bisa menerima QR BRI tanpa perubahan kode.

Contoh meminta kanal BRI secara eksplisit:

curl -X POST 'https://webqris.com/api/payments/qris/create'   -H 'Authorization: Bearer API_TOKEN_ANDA'   -H 'Content-Type: application/json'   -d '{
    "amount": 25000,
    "channel": "bri_merchant",
    "merchant_order_id": "ORDER-001"
  }'
Kalau kanal BRI sedang bermasalah

Bila kredensial/sesi BRI bermasalah atau QR gagal diterbitkan, permintaan tanpa channel otomatis dilayani kanal GoBiz agar transaksi tidak berhenti, dan invoice ditandai supaya jejaknya terlihat:

channel pada responsKanal yang benar-benar dipakai (mis. "gobiz").
requested_channelKanal yang diminta/diinginkan (mis. "bri_merchant") — hanya muncul saat terjadi pengalihan.
fallback_reasonAlasan pengalihan, mis. “Bridge BRI Merchant gagal: …”.
Transparansi: sama seperti GoBiz, kanal BRI Merchant membaca transaksi dari portal merchant BRI milik Anda. WebQRIS tetap bukan payment gateway dan tidak menampung dana. Pastikan Anda berhak memakai akun tersebut.
INFO Persiapan Integrasi — 4 nilai yang harus disiapkan

Keempat nilai ini yang menyambungkan sistem kamu dengan WebQRIS. Nilai aslinya ada di dashboard pada halaman Merchant Detail merchant yang bersangkutan: Webhook URL, Webhook Secret, dan TOKEN bisa disalin dari sana. API Endpoint selalu sama untuk semua merchant.

1. Webhook URLURL di sistem kamu sendiri yang menerima notifikasi pembayaran. Isi di halaman Merchant Detail.
Contoh: https://domain.com/webhook/qrispayment
2. Webhook SecretKunci untuk membuktikan notifikasi benar-benar datang dari WebQRIS, dipakai menghitung HMAC-SHA256. Bentuknya berawalan wh_.
Contoh: wh_••••••••••••••••••••••••••••••••
3. API EndpointAlamat untuk membuat invoice QRIS.
POST https://webqris.com/api/payments/qris/create
4. TOKENAPI token milik merchant, dipasang sebagai Authorization: Bearer <TOKEN>. Bentuknya {id}|{random}.
Contoh: xx|••••••••••••••••••••••••••••••••••••••••
Jangan tertukar arahnya. Webhook URL dan Webhook Secret adalah milik kamu — WebQRIS yang mengirim ke sana. API Endpoint dan TOKEN adalah milik WebQRIS — kamu yang mengirim ke sana.
Alur komunikasi lewat webhook
1Siapkan satu endpoint di sistem kamu yang menerima POST berisi JSON. Pastikan tidak butuh login/cookie, karena yang memanggil adalah server WebQRIS.
2Isi Webhook URL dan Webhook Secret di halaman Merchant Detail. Selama Webhook URL kosong, notifikasi tidak bisa dikirim dan job-nya berakhir gagal.
3Saat invoice berubah menjadi paid, WebQRIS mengirim POST event payment.paid ke URL itu, dengan header X-Webhook-Signature.
4Sistem kamu menghitung HMAC-SHA256 dari body mentah memakai Webhook Secret, lalu membandingkannya dengan header tersebut. Bandingkan dengan perbandingan waktu-tetap (hash_equals / timingSafeEqual).
5Balas 2xx kalau sudah diproses. Selain 2xx dianggap gagal dan WebQRIS mengulang otomatis — lihat kebijakan retry di bagian Webhook Outbound.
6Pakai invoice_id sebagai kunci idempotensi, karena percobaan ulang mengirim body yang sama.
Webhook itu opsional tapi disarankan. Tanpa Webhook URL, integrasi tetap jalan — kamu hanya perlu memeriksa status invoice sendiri lewat GET /api/payments/:invoiceId/status.
Ringkasan Integrasi
Aplikasi merchant / backend AndaPanggil POST /api/payments/qris/create dan GET /api/payments/:invoiceId/status dengan Authorization: Bearer YOUR_API_TOKEN.
Server Anda menerima notifikasi dari WebQRISSet Webhook URL di merchant detail. Itu adalah endpoint milik sistem Anda sendiri. WebQRIS akan POST ke sana saat pembayaran sukses.
APK / forwarder mengirim notifikasi ke WebQRISPakai POST /api/webhook/payment atau POST /api/callback/notify dengan Callback Secret. Dua endpoint ini adalah endpoint milik WebQRIS.
GoBiz detector server-sideUntuk merchant GoPay Merchant / GoBiz, WebQRIS dapat membaca transaksi QRIS dari GoBiz source yang terhubung. Callback APK otomatis diabaikan pada merchant yang memakai GoBiz detector aktif agar tidak double paid.
BRI Merchant server-sideUntuk merchant BRI Merchant, WebQRIS menerbitkan QR dinamis (±5 menit, tanpa kode unik) dari akun BRI milik merchant dan menandai invoice paid dari webhook refnum. Callback APK juga diabaikan pada merchant yang memakai kanal ini. Detail: BRI Merchant.
Aturan cepat agar tidak tertukar: API Token untuk backend merchant membuat invoice, Webhook URL adalah URL tujuan di server Anda, dan Callback Secret untuk autentikasi notif masuk ke WebQRIS.
Quick Start
  1. Buat merchant di dashboard, lalu generate minimal satu API Token.
  2. Jika ingin sistem Anda menerima status bayar otomatis, isi Webhook URL di merchant detail.
  3. Dari backend merchant Anda, panggil POST /api/payments/qris/create untuk membuat invoice dan tampilkan QR ke pelanggan.
  4. Jika Anda memakai APK Notification Forwarder, arahkan APK ke POST /api/webhook/payment dan isi Callback Secret.
  5. Jika memakai GoBiz detector, hubungkan GoBiz Source di halaman merchant. Webhook outbound ke sistem Anda tetap memakai format payment.paid yang sama.
  6. Jika memakai BRI Merchant, tambahkan koneksi di menu BRI Merchant, hubungkan ke merchant, lalu aktifkan kanalnya. Format webhook ke sistem Anda tetap sama (payment.paid) — hanya channel dan masa berlaku QR yang berbeda.
  7. Setelah payment sukses, WebQRIS akan mengirim webhook outbound ke Webhook URL Anda. Poll status via API hanya opsional.
Istilah Penting
API TokenKredensial untuk backend merchant Anda saat membuat invoice QRIS dan cek status via API.
Webhook URLURL tujuan di server Anda sendiri. WebQRIS akan mengirim notifikasi pembayaran sukses ke URL ini.
Callback SecretSecret untuk autentikasi notif masuk ke WebQRIS dari APK atau forwarder lain.
GoBiz SourceKoneksi akun GoPay Merchant / GoBiz yang dipakai WebQRIS untuk membaca settlement QRIS secara server-side.
BRI Merchant SourceKoneksi akun BRI Merchant (brimerchant.bri.co.id) milik merchant, dipakai WebQRIS untuk menerbitkan QR dinamis dan membaca pembayarannya secara server-side. Satu akun hanya boleh punya satu pemegang sesi.
channelKanal yang melayani invoice: gobiz atau bri_merchant. Bisa diminta saat create; kalau tidak dikirim, dipakai kanal default merchant.
payment_method: gobiz_apiStatus pembayaran berasal dari kanal GoBiz. Nama issuer e-wallet seperti DANA, OVO, atau SHOPEEPAY disimpan dari data GoBiz.
payment_method: bri_merchantStatus pembayaran berasal dari kanal BRI Merchant (dicocokkan lewat refnum, tanpa kode unik).
payment_method_labelLabel siap tampil dari WebQRIS, mis. QRIS DANA atau QRIS GOPAY — diambil dari issuer pengirim untuk semua kanal. Pakai nilai ini kalau sistem Anda menampilkan metode pembayaran ke pengguna.
Invoice IDID transaksi dari WebQRIS. Dipakai untuk cek status pembayaran.
merchant_order_idID order dari sistem Anda sendiri. Optional, tapi disarankan agar transaksi mudah dicocokkan.
POST /api/payments/qris/create

Buat transaksi QRIS baru. Akan mengembalikan QRIS payload yang bisa di-generate menjadi QR code.

Gunakan endpoint ini dari aplikasi merchant/server untuk membuat invoice QRIS. Pakai bersama header Authorization: Bearer YOUR_API_TOKEN. Ini bukan URL untuk APK Notification Forwarder dan bukan webhook outbound ke sistem Anda.
Headers
AuthorizationBearer YOUR_API_TOKENRequired
Content-Typeapplication/json
Request Body
{
  "amount": 25000,
  "merchant_order_id": "ORDER-001",
  "customer_name": "John Doe",
  "channel": "bri_merchant"
}
channel opsional. Isi "bri_merchant" atau "gobiz" bila ingin menentukan kanal sendiri; kalau dikosongkan, dipakai kanal default merchant (lihat BRI Merchant untuk detail tiap kanal).
Contoh cURL
curl -X POST 'https://webqris.com/api/payments/qris/create'   -H 'Authorization: Bearer YOUR_API_TOKEN'   -H 'Content-Type: application/json'   -d '{
    "amount": 25000,
    "merchant_order_id": "ORDER-001",
    "customer_name": "John Doe"
  }'
amountintegerNominal pembayaran (Rupiah)Required
merchant_order_idstringID order dari merchantOptional
customer_namestringNama pelangganOptional
channelstringbri_merchant atau gobiz. Kosongkan untuk memakai kanal default merchant.Optional
Response (201)
{
  "success": true,
  "invoice_id": "INV-1710000000-abc123",
  "channel": "bri_merchant",
  "qris_payload": "0002010211...",
  "qr_data_url": "data:image/png;base64,iVBORw0KGgo...",
  "amount": 25000,
  "unique_code": 0,
  "total_amount": 25000,
  "expired_at": "2026-03-09T10:30:00.000Z"
}
channelKanal yang benar-benar melayani invoice (gobiz atau bri_merchant).
unique_code0 pada kanal BRI Merchant (pencocokan lewat refnum), 1–36 pada kanal GoBiz.
expired_atMasa berlaku QR. Kanal BRI Merchant ±5 menit mengikuti acquirer; kanal GoBiz mengikuti setelan merchant.
refnum, qr_expires_atHanya muncul pada kanal BRI Merchant — nomor referensi QR dari BRI dan masa berlakunya.
requested_channel, fallback_reasonMuncul hanya bila permintaan kanal BRI dialihkan ke GoBiz (lihat BRI Merchant).
Kalau gagal
400amount kosong, nol, negatif, atau melebihi batas maksimum Rp 50.000.000. Juga muncul bila channel:"bri_merchant" diminta eksplisit tetapi merchant belum ditautkan ke koneksi BRI.
401Header Authorization tidak ada, bukan format Bearer …, token sudah di-revoke, atau merchant tidak aktif.
402Saldo pemilik merchant tidak cukup untuk menutup fee transaksi (setelah kuota gratis harian habis). Isi saldo lewat menu Top-Up, atau minta admin menandai akun sebagai exempt.
404Invoice tidak ditemukan, atau invoice itu milik merchant lain sehingga tidak terlihat oleh token yang dipakai.
503QRIS template merchant belum diatur, seluruh kode unik (1–36) sedang terpakai oleh invoice yang belum selesai, atau channel:"bri_merchant" diminta eksplisit sementara kanal BRI nonaktif/gagal diterbitkan. Permintaan tanpa channel tidak mengembalikan 503 — dialihkan otomatis ke GoBiz.
500Kesalahan internal. Coba ulang; kalau berulang hubungi admin.
Perhatikan sebelum retry. Setiap panggilan yang berhasil selalu melahirkan invoice baru — belum ada kunci idempotensi, dan merchant_order_id tidak dijamin unik. Jadi kalau request timeout lalu kamu ulangi panggilan yang sama, akan ada dua invoice berbeda (kode uniknya pun berbeda). Simpan invoice_id dari respons, dan sebelum membuat ulang pastikan dulu lewat GET /api/payments/:invoiceId/status atau dashboard.
Contoh Framework Backend: Create Invoice
PHP
<?php

  $payload = json_encode([
    'amount' => 25000,
    'merchant_order_id' => 'ORDER-001',
    'customer_name' => 'John Doe',
  ]);

  $ch = curl_init('https://webqris.com/api/payments/qris/create');
  curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
      'Authorization: Bearer YOUR_API_TOKEN',
      'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => $payload,
  ]);

  $response = curl_exec($ch);
  $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  curl_close($ch);

  $data = json_decode($response, true);
JavaScript / Node.js (Express)
app.post('/payments/create-qris', async (req, res) => {
  const response = await fetch('https://webqris.com/api/payments/qris/create', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer ' + process.env.WEBQRIS_API_TOKEN,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      amount: req.body.amount,
      merchant_order_id: req.body.merchant_order_id,
      customer_name: req.body.customer_name,
    }),
  });

  const data = await response.json();
  return res.status(response.status).json(data);
});
React / Next.js (aman via backend sendiri)
// app/api/webqris/create/route.ts
export async function POST(request: Request) {
  const body = await request.json();

  const response = await fetch('https://webqris.com/api/payments/qris/create', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer ' + process.env.WEBQRIS_API_TOKEN,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(body),
  });

  return Response.json(await response.json(), { status: response.status });
}

// Komponen React memanggil backend Anda sendiri, bukan WebQRIS langsung.
const resp = await fetch('/api/webqris/create', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ amount: 25000, merchant_order_id: 'ORDER-001' }),
});
Laravel
<?php

  namespace AppHttpControllers;

  use IlluminateHttpRequest;
  use IlluminateSupportFacadesHttp;

  class WebqrisPaymentController extends Controller
  {
    public function create(Request $request)
    {
      $response = Http::withToken(config('services.webqris.token'))
        ->post(config('services.webqris.base_url') . '/api/payments/qris/create', [
          'amount' => (int) $request->input('amount'),
          'merchant_order_id' => $request->input('merchant_order_id'),
          'customer_name' => $request->input('customer_name'),
        ]);

      return response()->json($response->json(), $response->status());
    }
  }
CodeIgniter 4
<?php

  namespace AppControllers;

  use CodeIgniterHTTPResponseInterface;
  use ConfigServices;

  class WebqrisPaymentController extends BaseController
  {
    public function create(): ResponseInterface
    {
      $client = Services::curlrequest();
      $response = $client->post(env('webqris.baseUrl') . '/api/payments/qris/create', [
        'headers' => [
          'Authorization' => 'Bearer ' . env('webqris.apiToken'),
          'Content-Type' => 'application/json',
        ],
        'json' => [
          'amount' => (int) $this->request->getJSON(true)['amount'],
          'merchant_order_id' => $this->request->getJSON(true)['merchant_order_id'] ?? null,
          'customer_name' => $this->request->getJSON(true)['customer_name'] ?? null,
        ],
      ]);

      return $this->response
        ->setStatusCode($response->getStatusCode())
        ->setJSON(json_decode($response->getBody(), true));
    }
  }
GET /api/payments/:invoiceId/status

Cek status pembayaran berdasarkan invoice ID.

Headers
AuthorizationBearer YOUR_API_TOKENRequired
Contoh cURL
curl -X GET 'https://webqris.com/api/payments/INV-1710000000-abc123/status'   -H 'Authorization: Bearer YOUR_API_TOKEN'
Response (200)
{
  "success": true,
  "data": {
    "invoice_id": "INV-MERCHANT-1710000000-abc123",
    "merchant_order_id": "ORDER-001",
    "qris_payload": "0002010211...",
    "qr_data_url": "data:image/png;base64,iVBORw0KGgo...",
    "amount": 25000,
    "unique_code": 42,
    "total_amount": 25042,
    "status": "paid",
    "payment_method": "gobiz_api",
    "payment_method_label": "QRIS DANA",
    "paid_at": "2026-03-09T10:15:23.000Z",
    "expired_at": "2026-03-09T10:30:00.000Z"
  }
}
Catatan payment_method: nilai gobiz_api berarti pembayaran diproses oleh kanal GoBiz, bri_merchant berarti kanal BRI Merchant. Untuk jalur APK lama, nilainya bisa berupa package aplikasi sumber notifikasi seperti com.dana.id atau com.gojek.gopaymerchant. Gunakan payment_method_label (mis. QRIS DANA) untuk ditampilkan ke pengguna. Field waktu API memakai ISO timestamp. Tampilan WIB dipakai di dashboard dan notifikasi WhatsApp.
Kalau gagal
401Header Authorization tidak ada atau token tidak valid.
404Invoice tidak ditemukan — bisa karena salah invoice_id, atau invoice itu milik merchant lain sehingga tidak terlihat oleh token yang dipakai.
Contoh Framework Backend: Check Status
PHP
<?php

  $invoiceId = 'INV-1710000000-abc123';

  $ch = curl_init('https://webqris.com/api/payments/' . urlencode($invoiceId) . '/status');
  curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
      'Authorization: Bearer YOUR_API_TOKEN',
    ],
  ]);

  $response = curl_exec($ch);
  $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  curl_close($ch);

  $data = json_decode($response, true);
JavaScript / Node.js (Express)
app.get('/payments/:invoiceId/status', async (req, res) => {
  const response = await fetch(
    'https://webqris.com/api/payments/' + encodeURIComponent(req.params.invoiceId) + '/status',
    {
      headers: {
        'Authorization': 'Bearer ' + process.env.WEBQRIS_API_TOKEN,
      },
    }
  );

  const data = await response.json();
  return res.status(response.status).json(data);
});
React / Next.js (aman via backend sendiri)
// app/api/webqris/status/[invoiceId]/route.ts
export async function GET(
  _request: Request,
  { params }: { params: { invoiceId: string } }
) {
  const response = await fetch(
    'https://webqris.com/api/payments/' + encodeURIComponent(params.invoiceId) + '/status',
    {
      headers: {
        'Authorization': 'Bearer ' + process.env.WEBQRIS_API_TOKEN,
      },
      cache: 'no-store',
    }
  );

  return Response.json(await response.json(), { status: response.status });
}

// Komponen React cukup memanggil endpoint backend Anda sendiri.
const resp = await fetch('/api/webqris/status/' + invoiceId);
Laravel
<?php

  namespace AppHttpControllers;

  use IlluminateSupportFacadesHttp;

  class WebqrisStatusController extends Controller
  {
    public function show(string $invoiceId)
    {
      $response = Http::withToken(config('services.webqris.token'))
        ->get(config('services.webqris.base_url') . '/api/payments/' . $invoiceId . '/status');

      return response()->json($response->json(), $response->status());
    }
  }
CodeIgniter 4
<?php

  namespace AppControllers;

  use CodeIgniterHTTPResponseInterface;
  use ConfigServices;

  class WebqrisStatusController extends BaseController
  {
    public function show(string $invoiceId): ResponseInterface
    {
      $client = Services::curlrequest();
      $response = $client->get(env('webqris.baseUrl') . '/api/payments/' . $invoiceId . '/status', [
        'headers' => [
          'Authorization' => 'Bearer ' . env('webqris.apiToken'),
        ],
      ]);

      return $this->response
        ->setStatusCode($response->getStatusCode())
        ->setJSON(json_decode($response->getBody(), true));
    }
  }
POST YOUR_WEBHOOK_URL

WebQRIS akan mengirim webhook ke URL yang Anda konfigurasi saat pembayaran berhasil. Ini adalah endpoint milik sistem Anda sendiri, bukan endpoint milik WebQRIS.

Headers
X-Webhook-SignatureHMAC-SHA256 signature dari body dengan webhook_secret
X-SignatureSama dengan di atas (legacy header, untuk backward compatibility)
Content-Typeapplication/json
User-AgentWebQRIS-Webhook/2.0
Webhook Body
{
  "event": "payment.paid",
  "data": {
    "invoice_id": "INV-MERCHANT-1710000000-abc123",
    "merchant_order_id": "ORDER-001",
    "amount": 25000,
    "unique_code": 0,
    "total_amount": 25000,
    "customer_name": "John Doe",
    "status": "paid",
    "payment_method": "bri_merchant",
    "issuer": "DANA",
    "sender_name": "DANA",
    "funding_source": "DANA",
    "paid_at": "2026-03-09T10:15:23.000Z"
  }
}
payment_methodKanal/sumber paid: gobiz_api (kanal GoBiz) atau bri_merchant (kanal BRI Merchant). Pada merchant yang lebih lama bisa berupa package aplikasi, mis. com.gojek.gopaymerchant.
channelTersedia pada endpoint status: gobiz atau bri_merchant.
unique_code0 bila kanalnya BRI Merchant (dicocokkan lewat refnum); 1–36 pada kanal GoBiz.
issuer / sender_name / funding_sourceInfo pengirim dari gateway, mis. DANA, GOPAY, SHOPEEPAY, atau nama bank. Bisa juga muncul menyusul pada event susulan payment.enriched.
Webhook outbound memakai format yang sama untuk semua sumber paid: APK Notification Forwarder, callback sederhana, manual confirm, kanal GoBiz, maupun kanal BRI Merchant. Untuk menampilkan metode pembayaran ke pengguna, pakai label siap pakai payment_method_label dari endpoint status (mis. QRIS DANA) — jangan menampilkan nilai mentah payment_method karena itu kode internal.
Pembayaran telat tetap diproses. Bila pembayaran masuk setelah invoice expired (khas kanal BRI Merchant yang QR-nya hanya ±5 menit), invoice tetap ditandai paid selama nominalnya cocok dan paid_at dari gateway tidak lebih awal dari pembuatan invoice. Karena itu klien disarankan mempercayai event webhook, bukan menganggap invoice expired sebagai batal permanen.
Retry Policy

Webhook dikirim lewat antrian permanen (webhook_jobs): job yang belum berhasil tetap tersimpan dan dicoba ulang walau server WebQRIS sempat restart. Percobaan dicatat di webhook_logs.

Timeout per percobaan10 detik
Maksimum percobaan8× (dapat diatur lewat WEBHOOK_MAX_ATTEMPTS)
Jeda antar percobaan10s, 20s, 40s, 80s, 160s, 320s, 640s — berlipat 2×, dibatasi maksimum 1 jam
Dianggap berhasilEndpoint kamu membalas HTTP 2xx
Setelah 8× gagalStatus job menjadi dead dan berhenti dicoba

Satu invoice hanya menghasilkan satu job webhook, jadi kalau kamu menerima payload yang sama dua kali itu memang percobaan ulang — pakai invoice_id sebagai kunci idempotensi di sisi kamu.

Contoh Receiver Webhook (PHP)
<?php
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$body = file_get_contents('php://input');
$expected = hash_hmac('sha256', $body, 'YOUR_WEBHOOK_SECRET');

if (!hash_equals($expected, $signature)) {
    http_response_code(401);
    echo json_encode(['success' => false, 'message' => 'Invalid signature']);
    exit;
}

$payload = json_decode($body, true);
if (!is_array($payload) || ($payload['event'] ?? '') !== 'payment.paid') {
    http_response_code(400);
    echo json_encode(['success' => false, 'message' => 'Invalid event']);
    exit;
}

$payment = $payload['data'] ?? [];
$invoiceId = $payment['invoice_id'] ?? null;
$merchantOrderId = $payment['merchant_order_id'] ?? null;
$status = $payment['status'] ?? null;

// TODO: cocokan invoice ke database Anda, tandai paid, lalu proses order.

http_response_code(200);
header('Content-Type: application/json');
echo json_encode([
    'success' => true,
    'invoice_id' => $invoiceId,
    'merchant_order_id' => $merchantOrderId,
    'status' => $status,
]);
Contoh Receiver Webhook (Node.js / Express)
const express = require('express');
const crypto = require('crypto');

const app = express();

app.post('/webhook/qris', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-webhook-signature'] || '';
  const body = req.body.toString('utf8');
  const expected = crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET)
    .update(body)
    .digest('hex');

  if (signature !== expected) {
    return res.status(401).json({ success: false, message: 'Invalid signature' });
  }

  const payload = JSON.parse(body);
  if (payload.event !== 'payment.paid') {
    return res.status(400).json({ success: false, message: 'Invalid event' });
  }

  const payment = payload.data || {};
  const invoiceId = payment.invoice_id;
  const merchantOrderId = payment.merchant_order_id;
  const status = payment.status;

  // TODO: cocokan invoice ke database Anda, tandai paid, lalu proses order.

  return res.status(200).json({
    success: true,
    invoice_id: invoiceId,
    merchant_order_id: merchantOrderId,
    status,
  });
});
Contoh Receiver Webhook: React / Next.js
import crypto from 'node:crypto';

// app/api/webhook/qris/route.ts
export async function POST(request: Request) {
  const signature = request.headers.get('x-webhook-signature') ?? '';
  const body = await request.text();
  const expected = crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET || '')
    .update(body)
    .digest('hex');

  if (signature !== expected) {
    return Response.json({ success: false, message: 'Invalid signature' }, { status: 401 });
  }

  const payload = JSON.parse(body);
  if (payload.event !== 'payment.paid') {
    return Response.json({ success: false, message: 'Invalid event' }, { status: 400 });
  }

  const payment = payload.data || {};

  // TODO: update order di database Anda.

  return Response.json({
    success: true,
    invoice_id: payment.invoice_id,
    merchant_order_id: payment.merchant_order_id,
    status: payment.status,
  });
}

// React frontend tidak menerima webhook langsung.
// Frontend cukup membaca status dari backend Anda sendiri.
Contoh Receiver Webhook: Laravel
<?php

  use IlluminateHttpRequest;
  use IlluminateSupportFacadesLog;
  use IlluminateSupportFacadesRoute;

  Route::post('/webhook/qris', function (Request $request) {
    $signature = $request->header('X-Webhook-Signature', '');
    $body = $request->getContent();
    $expected = hash_hmac('sha256', $body, env('WEBHOOK_SECRET'));

    if (!hash_equals($expected, $signature)) {
      return response()->json([
        'success' => false,
        'message' => 'Invalid signature',
      ], 401);
    }

    $payload = json_decode($body, true);
    if (($payload['event'] ?? '') !== 'payment.paid') {
      return response()->json([
        'success' => false,
        'message' => 'Invalid event',
      ], 400);
    }

    $payment = $payload['data'] ?? [];

    // TODO: cocokkan invoice/order di database Anda.
    Log::info('WebQRIS payment paid', $payment);

    return response()->json([
      'success' => true,
      'invoice_id' => $payment['invoice_id'] ?? null,
      'merchant_order_id' => $payment['merchant_order_id'] ?? null,
      'status' => $payment['status'] ?? null,
    ]);
  });
Route + Controller Laravel (Versi Terpisah)
// routes/api.php
  use AppHttpControllersWebqrisWebhookController;

  Route::post('/webhook/qris', [WebqrisWebhookController::class, 'paid']);

  // app/Http/Controllers/WebqrisWebhookController.php
  namespace AppHttpControllers;

  use IlluminateHttpRequest;
  use IlluminateSupportFacadesLog;

  class WebqrisWebhookController extends Controller
  {
    public function paid(Request $request)
    {
      $signature = $request->header('X-Webhook-Signature', '');
      $body = $request->getContent();
      $expected = hash_hmac('sha256', $body, env('WEBHOOK_SECRET'));

      if (!hash_equals($expected, $signature)) {
        return response()->json(['success' => false, 'message' => 'Invalid signature'], 401);
      }

      $payload = json_decode($body, true);
      $payment = $payload['data'] ?? [];
      Log::info('WebQRIS payment paid', $payment);

      return response()->json(['success' => true]);
    }
  }
Contoh Receiver Webhook: CodeIgniter 4
<?php

  namespace AppControllers;

  use CodeIgniterHTTPResponseInterface;

  class WebqrisWebhook extends BaseController
  {
    public function paid(): ResponseInterface
    {
      $signature = $this->request->getHeaderLine('X-Webhook-Signature');
      $body = $this->request->getBody();
      $expected = hash_hmac('sha256', $body, env('webqris.webhookSecret'));

      if (!hash_equals($expected, $signature)) {
        return $this->response
          ->setStatusCode(401)
          ->setJSON([
            'success' => false,
            'message' => 'Invalid signature',
          ]);
      }

      $payload = json_decode($body, true);
      if (($payload['event'] ?? '') !== 'payment.paid') {
        return $this->response
          ->setStatusCode(400)
          ->setJSON([
            'success' => false,
            'message' => 'Invalid event',
          ]);
      }

      $payment = $payload['data'] ?? [];

      // TODO: cocokkan invoice/order di database Anda.

      return $this->response->setJSON([
        'success' => true,
        'invoice_id' => $payment['invoice_id'] ?? null,
        'merchant_order_id' => $payment['merchant_order_id'] ?? null,
        'status' => $payment['status'] ?? null,
      ]);
    }
  }
Route + Controller CodeIgniter 4 (Versi Terpisah)
// app/Config/Routes.php
    $routes->post('webhook/qris', 'WebqrisWebhook::paid');

    // app/Controllers/WebqrisWebhook.php
    namespace AppControllers;

    class WebqrisWebhook extends BaseController
    {
      public function paid()
      {
        $signature = $this->request->getHeaderLine('X-Webhook-Signature');
        $body = $this->request->getBody();
        $expected = hash_hmac('sha256', $body, env('webqris.webhookSecret'));

        if (!hash_equals($expected, $signature)) {
          return $this->response->setStatusCode(401)->setJSON([
            'success' => false,
            'message' => 'Invalid signature',
          ]);
        }

        $payload = json_decode($body, true);
        $payment = $payload['data'] ?? [];

        return $this->response->setJSON([
          'success' => true,
          'invoice_id' => $payment['invoice_id'] ?? null,
        ]);
      }
    }
POST /api/webhook/payment

Endpoint milik WebQRIS untuk menerima notifikasi e-wallet yang di-forward dari APK Notification Forwarder. Server akan mem-parse nominal dari field message, mencari payment pending dengan total_amount yang sama (per-merchant), lalu mengubah status menjadi paid dan mengirim webhook outbound ke sistem merchant (jika dikonfigurasi).

Jika merchant sudah terhubung ke GoBiz detector aktif, endpoint ini sengaja mengabaikan notifikasi APK agar satu pembayaran tidak diproses dua jalur.
Headers
AuthorizationBearer <CALLBACK_SECRET>Required
Content-Typeapplication/json
Request Body (contoh)
{
  "app": "com.dana.id",
  "title": "DANA",
  "message": "Kamu berhasil menerima Rp25.042 dari JOHN DOE",
  "timestamp": "2026-03-21T10:15:23.000Z",
  "device_id": "device-01",
  "notif_id": "1234567890",
  "event_hash": "..."
}
Parameter
messagestringTeks notifikasi. Nominal diparse dari sini, jadi bagian inilah yang harus benar.Required
appstringNama paket aplikasi sumber, misalnya com.dana.id. Dipakai menyaring notifikasi yang bukan pembayaran masuk.Optional
titlestringJudul notifikasi. Diperiksa bersama message.Optional
channel_idstringID kanal notifikasi dari APK.Optional
timestampstringWaktu notifikasi menurut perangkat pengirim.Optional
device_idstringPenanda perangkat pengirim, berguna untuk penelusuran.Optional
notif_idstringID notifikasi. Dipakai mencegah notifikasi yang sama diproses dua kali.Disarankan
event_hashstringPenanda unik event dari APK. Nilai test dipakai untuk uji koneksi.Optional
debug_gopayobjectData tambahan untuk penelusuran notifikasi GoPay.Optional
Respons
200Berhasil diproses, atau sengaja diabaikan — perhatikan penanda ignored dan reason pada body respons.
400Body bukan JSON yang valid, atau message tidak ada.
401Token tidak dikenal, atau merchant tidak aktif.
422Nominal tidak berhasil dibaca dari message.
404Tidak ada invoice pending dengan total_amount yang sama pada merchant tersebut.
Test Koneksi

Untuk test auth & koneksi tanpa memproses pembayaran, kirim event_hash bernilai test.

{
  "message": "test",
  "event_hash": "test"
}
Contoh cURL Test
curl -X POST 'https://webqris.com/api/webhook/payment'   -H 'Authorization: Bearer YOUR_CALLBACK_SECRET'   -H 'Content-Type: application/json'   -d '{
    "message": "test",
    "event_hash": "test"
  }'
Response (contoh)
{
  "success": true,
  "invoice_id": "INV-1710000000-abc123",
  "parsed_amount": 25042,
  "sender_name": "JOHN DOE"
}
Response — Test Mode (event_hash: "test")
{
  "success": true,
  "message": "Koneksi berhasil! Auth valid ✓ (Merchant: Toko ABC)",
  "test": true
}
Response — Duplicate Notification
{
  "success": false,
  "message": "Duplicate notification",
  "notif_id": "1234567890"
}
Response — Diabaikan karena GoBiz aktif
{
  "success": true,
  "ignored": true,
  "message": "Merchant memakai GoBiz detector; APK notification diabaikan"
}
Response — Gagal Parse Nominal (422)
{
  "success": false,
  "message": "Tidak dapat mendeteksi nominal dari notifikasi"
}
Response — Tidak Ada Payment Cocok (404)
{
  "success": false,
  "message": "No matching pending payment",
  "parsed_amount": 25042,
  "merchant": "Toko ABC"
}
POST /api/callback/notify

Endpoint milik WebQRIS dengan format sederhana untuk menandai payment sebagai paid berdasarkan nominal amount (harus sama dengan total_amount). Cocok untuk integrasi non-APK.

Jika merchant sudah memakai GoBiz detector aktif, callback sederhana ini juga diabaikan. Sumber paid yang dipakai adalah settlement GoBiz agar tidak ada double processing.
Headers
X-Callback-Key<CALLBACK_SECRET>Required
Content-Typeapplication/json
Request Body
{
  "amount": 25042,
  "sender_name": "JOHN DOE",
  "reference": "optional-reference"
}
Contoh cURL
curl -X POST 'https://webqris.com/api/callback/notify'   -H 'X-Callback-Key: YOUR_CALLBACK_SECRET'   -H 'Content-Type: application/json'   -d '{
    "amount": 25042,
    "sender_name": "JOHN DOE",
    "reference": "optional-reference"
  }'
Contoh Backend Kirim Callback Notify
PHP
<?php

  $payload = json_encode([
    'amount' => 25042,
    'sender_name' => 'JOHN DOE',
    'reference' => 'optional-reference',
  ]);

  $ch = curl_init('https://webqris.com/api/callback/notify');
  curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
      'X-Callback-Key: YOUR_CALLBACK_SECRET',
      'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => $payload,
  ]);

  $response = curl_exec($ch);
  curl_close($ch);
JavaScript / Node.js (Express)
app.post('/forward-qris-notify', async (req, res) => {
  const response = await fetch('https://webqris.com/api/callback/notify', {
    method: 'POST',
    headers: {
      'X-Callback-Key': process.env.WEBQRIS_CALLBACK_SECRET,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      amount: req.body.amount,
      sender_name: req.body.sender_name,
      reference: req.body.reference,
    }),
  });

  const data = await response.json();
  return res.status(response.status).json(data);
});
React / Next.js (aman via backend sendiri)
// app/api/webqris/callback-notify/route.ts
export async function POST(request: Request) {
  const body = await request.json();

  const response = await fetch('https://webqris.com/api/callback/notify', {
    method: 'POST',
    headers: {
      'X-Callback-Key': process.env.WEBQRIS_CALLBACK_SECRET || '',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(body),
  });

  return Response.json(await response.json(), { status: response.status });
}

// React frontend memanggil backend Anda sendiri, jangan expose callback secret ke browser.
Laravel
<?php

  use IlluminateSupportFacadesHttp;

  $response = Http::withHeaders([
    'X-Callback-Key' => config('services.webqris.callback_secret'),
  ])->post(config('services.webqris.base_url') . '/api/callback/notify', [
    'amount' => 25042,
    'sender_name' => 'JOHN DOE',
    'reference' => 'optional-reference',
  ]);

  $data = $response->json();
CodeIgniter 4
<?php

  $client = ConfigServices::curlrequest();
  $response = $client->post(env('webqris.baseUrl') . '/api/callback/notify', [
    'headers' => [
      'X-Callback-Key' => env('webqris.callbackSecret'),
      'Content-Type' => 'application/json',
    ],
    'json' => [
      'amount' => 25042,
      'sender_name' => 'JOHN DOE',
      'reference' => 'optional-reference',
    ],
  ]);

  $data = json_decode($response->getBody(), true);
Kalau gagal
400Field amount tidak ada atau nilainya tidak lebih besar dari nol.
401Header X-Callback-Key tidak ada, atau nilainya bukan Callback Secret milik merchant mana pun.
404Tidak ada invoice pending pada merchant tersebut yang total_amount-nya sama dengan amount.
Response
{
  "success": true,
  "invoice_id": "INV-1710000000-abc123"
}
Response — Diabaikan karena GoBiz aktif
{
  "success": true,
  "ignored": true,
  "message": "Merchant memakai GoBiz detector; APK notification diabaikan"
}
WS /ws  &  /ws/merchant?token=API_TOKEN

WebSocket untuk menerima event realtime. /ws untuk dashboard dengan session login aktif, /ws/user untuk user dengan session login aktif, dan /ws/merchant untuk merchant-specific dengan API token pada query param token.

Authentication: /ws dan /ws/user menggunakan session cookie same-origin dari login WebQRIS. Koneksi tanpa session aktif ditolak dengan HTTP 401. /ws/merchant menggunakan token=API_TOKEN dan juga akan ditolak dengan 401 jika token tidak valid.
Event scope: koneksi dashboard dengan role admin atau superadmin menerima event global. Role client dan operator hanya menerima event untuk owner yang sama. /ws/user adalah channel user dan tidak diperlakukan sebagai viewer dashboard.
Event dan channel-nya
EventDikirim keKeterangan
payment_paid/ws, /ws/merchantPembayaran berhasil (dari APK, callback, GoBiz detector, atau konfirmasi manual). Satu-satunya event yang juga dikirim ke /ws/merchant.
new_payment/wsInvoice baru dibuat.
payments_expired/wsInvoice melewati batas waktu (diproses berkala, setiap 60 detik).
payment_deleted/wsInvoice dihapus — baik dibersihkan otomatis setelah masa tenggang, maupun dihapus manual.
webhook_delivered/wsWebhook outbound berhasil terkirim ke sistem merchant.
merchant_changed/wsData merchant berubah (dibuat, diubah, atau dihapus).
Kalau integrasi kamu hanya butuh realtime pembayaran: pakai /ws/merchant?token=API_TOKEN dan tunggu event payment_paid. Event dashboard lainnya (new_payment, payment_deleted, dan seterusnya) tidak dikirim ke channel merchant.
Format Message
{
  "event": "payment_paid",
  "data": {
    "id": 123,
    "invoice_id": "INV-1710000000-abc123",
    "amount": 25000,
    "total_amount": 25042,
    "unique_code": 42,
    "merchant_name": "Toko ABC",
    "sender_name": "JOHN DOE",
    "funding_source": "DANA",
    "source": "gobiz_api",
    "status": "paid"
  },
  "ts": 1710000000000
}
Ping/Pong

Kirim pesan ping → server membalas pong. Gunakan untuk keep-alive.

Callback settlement

Transisi payment menjadi paid dan settlement saldo diproses secara atomik. Callback yang dikirim bersamaan untuk invoice yang sama hanya boleh menghasilkan satu settlement dan satu rangkaian event; callback yang kalah race dapat menerima acknowledgement sukses tanpa membuat kredit atau fee kedua.

Contoh (JavaScript)
const ws = new WebSocket('wss://webqris.com/ws/merchant?token=YOUR_API_TOKEN');
ws.onmessage = (e) => {
  const { event, data } = JSON.parse(e.data);
  if (event === 'payment_paid') {
    console.log('Pembayaran masuk:', data.invoice_id, data.total_amount);
  }
};
// Keep-alive
setInterval(() => ws.send('ping'), 30000);
ERROR Error Responses

Semua endpoint mengembalikan format error yang konsisten:

{
  "success": false,
  "message": "Deskripsi error"
}
400Bad Request — parameter tidak valid atau kurang.
401Unauthorized — token tidak valid, tidak ada, atau merchant tidak aktif.
402Payment Required — saldo pemilik merchant tidak cukup untuk menutup fee transaksi (setelah kuota gratis harian habis). Isi saldo lewat menu Top-Up, atau minta admin menandai akun sebagai exempt.
404Not Found — resource tidak ditemukan, atau milik merchant lain sehingga tidak terlihat oleh token yang dipakai.
422Unprocessable — gagal memproses data (contoh: nominal tidak terdeteksi dari notifikasi).
500Internal Server Error.
503Service Unavailable — QRIS belum dikonfigurasi, atau seluruh kode unik (1–36) sedang terpakai.
Alur Integrasi
  1. Register & login ke WebQRIS Dashboard
  2. Buat Merchant dan generate API Token
  3. Set Webhook URL untuk menerima notifikasi pembayaran
  4. Pilih sumber deteksi pembayaran: APK Notification Forwarder, callback sederhana, atau GoBiz detector server-side.
  5. Kirim request POST /api/payments/qris/create dari backend Anda
  6. Tampilkan QR Code dari qris_payload ke pelanggan
  7. Jika memakai APK / notif forwarder, kirim notifikasi masuk ke endpoint WebQRIS: POST /api/webhook/payment atau POST /api/callback/notify
  8. Jika memakai GoBiz detector, hubungkan GoBiz Source di dashboard merchant. Untuk merchant tersebut, callback APK akan diabaikan dan settlement GoBiz menjadi sumber paid.
  9. Poll status via GET /api/payments/:invoiceId/status (opsional)
  10. Terima webhook outbound dari WebQRIS di Webhook URL milik sistem Anda saat status menjadi paid
  11. Verifikasi HMAC signature dan proses pembayaran
WebQRIS v3.6 — Dashboard