https://webqris.comAuthorization: Bearer YOUR_API_TOKENwss://webqris.com/ws (dashboard) | wss://webqris.com/ws/merchant?token=API_TOKEN (merchant)
| Aplikasi merchant / backend Anda | Panggil POST /api/payments/qris/create dan GET /api/payments/:invoiceId/status dengan Authorization: Bearer YOUR_API_TOKEN. |
| Server Anda menerima notifikasi dari WebQRIS | Set 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 WebQRIS | Pakai POST /api/webhook/payment atau POST /api/callback/notify dengan Callback Secret. Dua endpoint ini adalah endpoint milik WebQRIS. |
API Token untuk backend merchant membuat invoice, Webhook URL adalah URL tujuan di server Anda, dan Callback Secret untuk autentikasi notif masuk ke WebQRIS.
POST /api/payments/qris/create untuk membuat invoice dan tampilkan QR ke pelanggan.POST /api/webhook/payment dan isi Callback Secret.API Token | Kredensial untuk backend merchant Anda saat membuat invoice QRIS dan cek status via API. |
Webhook URL | URL tujuan di server Anda sendiri. WebQRIS akan mengirim notifikasi pembayaran sukses ke URL ini. |
Callback Secret | Secret untuk autentikasi notif masuk ke WebQRIS dari APK atau forwarder lain. |
Invoice ID | ID transaksi dari WebQRIS. Dipakai untuk cek status pembayaran. |
merchant_order_id | ID order dari sistem Anda sendiri. Optional, tapi disarankan agar transaksi mudah dicocokkan. |
Buat transaksi QRIS baru. Akan mengembalikan QRIS payload yang bisa di-generate menjadi QR code.
Authorization: Bearer YOUR_API_TOKEN. Ini bukan URL untuk APK Notification Forwarder dan bukan webhook outbound ke sistem Anda.
Authorization | Bearer YOUR_API_TOKEN | Required |
Content-Type | application/json |
{
"amount": 25000,
"merchant_order_id": "ORDER-001",
"customer_name": "John Doe"
}
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"
}'
amount | integer | Nominal pembayaran (Rupiah) | Required |
merchant_order_id | string | ID order dari merchant | Optional |
customer_name | string | Nama pelanggan | Optional |
{
"success": true,
"invoice_id": "INV-1710000000-abc123",
"qris_payload": "0002010211...",
"amount": 25000,
"unique_code": 42,
"total_amount": 25042,
"expired_at": "2026-03-09T10:30:00.000Z"
}
<?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);
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);
});
// 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' }),
});
<?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());
}
}
<?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));
}
}
Cek status pembayaran berdasarkan invoice ID.
Authorization | Bearer YOUR_API_TOKEN | Required |
curl -X GET 'https://webqris.com/api/payments/INV-1710000000-abc123/status' -H 'Authorization: Bearer YOUR_API_TOKEN'
{
"success": true,
"data": {
"invoice_id": "INV-MERCHANT-1710000000-abc123",
"merchant_order_id": "ORDER-001",
"qris_payload": "0002010211...",
"amount": 25000,
"unique_code": 42,
"total_amount": 25042,
"status": "paid",
"payment_method": "com.dana.id",
"paid_at": "2026-03-09T10:15:23.000Z",
"expired_at": "2026-03-09T10:30:00.000Z"
}
}
<?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);
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);
});
// 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);
<?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());
}
}
<?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));
}
}
WebQRIS akan mengirim webhook ke URL yang Anda konfigurasi saat pembayaran berhasil. Ini adalah endpoint milik sistem Anda sendiri, bukan endpoint milik WebQRIS.
X-Webhook-Signature | HMAC-SHA256 signature dari body dengan webhook_secret |
X-Signature | Sama dengan di atas (legacy header, untuk backward compatibility) |
Content-Type | application/json |
User-Agent | WebQRIS-Webhook/1.0 |
{
"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": "com.dana.id",
"paid_at": "2026-03-09T10:15:23.000Z"
}
}
Webhook akan di-retry hingga 3× jika gagal (HTTP status non-2xx atau timeout). Delay antar retry menggunakan exponential backoff: 2s, 4s, 8s. Timeout per request: 10 detik.
<?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,
]);
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,
});
});
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.
<?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,
]);
});
// 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]);
}
}
<?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,
]);
}
}
// 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,
]);
}
}
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).
Authorization | Bearer <CALLBACK_SECRET> | Required |
Content-Type | application/json |
{
"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": "..."
}
Untuk test auth & koneksi tanpa memproses pembayaran, kirim event_hash bernilai test.
{
"message": "test",
"event_hash": "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"
}'
{
"success": true,
"invoice_id": "INV-1710000000-abc123",
"parsed_amount": 25042,
"sender_name": "JOHN DOE"
}
event_hash: "test"){
"success": true,
"message": "Koneksi berhasil! Auth valid ✓ (Merchant: Toko ABC)",
"test": true
}
{
"success": false,
"message": "Duplicate notification",
"notif_id": "1234567890"
}
{
"success": false,
"message": "Tidak dapat mendeteksi nominal dari notifikasi",
"raw_message": "Pesan yang tidak dikenal"
}
{
"success": false,
"message": "No matching pending payment",
"parsed_amount": 25042,
"merchant": "Toko ABC"
}
Endpoint milik WebQRIS dengan format sederhana untuk menandai payment sebagai paid berdasarkan nominal amount (harus sama dengan total_amount). Cocok untuk integrasi non-APK.
X-Callback-Key | <CALLBACK_SECRET> | Required |
Content-Type | application/json |
{
"amount": 25042,
"sender_name": "JOHN DOE",
"reference": "optional-reference"
}
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"
}'
<?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);
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);
});
// 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.
<?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();
<?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);
{
"success": true,
"invoice_id": "INV-1710000000-abc123"
}
WebSocket untuk menerima event realtime. /ws untuk dashboard admin, /ws/merchant untuk merchant-specific (auth via query param token).
new_payment | Payment baru dibuat |
payment_paid | Payment berhasil dibayar (dari APK, callback, atau manual confirm) |
payments_expired | Payment expired (batch, tiap 60 detik) |
webhook_delivered | Webhook outbound berhasil terkirim ke merchant |
{
"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",
"status": "paid"
},
"ts": 1710000000000
}
Kirim pesan ping → server membalas pong. Gunakan untuk keep-alive.
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);
Semua endpoint mengembalikan format error yang konsisten:
{
"success": false,
"message": "Deskripsi error"
}
400 | Bad Request — parameter tidak valid atau kurang |
401 | Unauthorized — token tidak valid atau tidak ada |
404 | Not Found — resource tidak ditemukan |
422 | Unprocessable — gagal memproses data (contoh: nominal tidak terdeteksi) |
500 | Internal Server Error |
503 | Service Unavailable — QRIS belum dikonfigurasi atau kode unik habis |
POST /api/payments/qris/create dari backend Andaqris_payload ke pelangganPOST /api/webhook/payment atau POST /api/callback/notifyGET /api/payments/:invoiceId/status (opsional)paid