Doc Updated — 20 September 2026, 18:43 WIB
Versi inilah yang berlaku. Jangan mengacu ke salinan atau backup dokumentasi lama.

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 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 GoBiz detector.
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 GoBiz detector aktif.
webhook outboundTetap memakai event payment.paid dan signature HMAC yang sama seperti jalur APK/callback.
Contoh data paid GoBiz
{
  "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",
    "funding_source": "DANA",
    "paid_at": "2026-03-09T10:15:23.000Z"
  }
}
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 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.
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. 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.
payment_method: gobiz_apiStatus pembayaran berasal dari GoBiz detector. Nama issuer e-wallet seperti DANA, OVO, atau SHOPEEPAY disimpan dari data GoBiz dan dipakai pada notifikasi WhatsApp.
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"
}
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
Response (201)
{
  "success": true,
  "invoice_id": "INV-1710000000-abc123",
  "qris_payload": "0002010211...",
  "qr_data_url": "data:image/png;base64,iVBORw0KGgo...",
  "amount": 25000,
  "unique_code": 42,
  "total_amount": 25042,
  "expired_at": "2026-03-09T10:30:00.000Z"
}
Kalau gagal
400amount kosong, nol, negatif, atau melebihi batas maksimum Rp 50.000.000.
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, atau seluruh kode unik (1–36) sedang terpakai oleh invoice yang belum selesai.
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",
    "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 GoBiz detector. Untuk jalur APK lama, nilainya bisa berupa package aplikasi sumber notifikasi seperti com.dana.id atau com.gojek.gopaymerchant. 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": 42,
    "total_amount": 25042,
    "customer_name": "John Doe",
    "status": "paid",
    "payment_method": "gobiz_api",
    "funding_source": "DANA",
    "paid_at": "2026-03-09T10:15:23.000Z"
  }
}
Webhook outbound memakai format yang sama untuk semua sumber paid: APK Notification Forwarder, callback sederhana, manual confirm, maupun GoBiz detector. Pada GoBiz detector, payment_method bernilai gobiz_api.
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