Integrasi Webhook Payment Gateway
Hubungkan HP kasir Android Anda dengan server backend secara langsung (P2P). Tanpa server perantara, data mutasi Anda aman 100%.
apiPayload Parameters (JSON)
Setiap kali terdeteksi mutasi uang masuk, EBayar akan mengirimkan request HTTP POST dengan format payload JSON berikut:
| Field | Tipe | Keterangan |
|---|---|---|
| amount | Integer | Nominal uang masuk (Contoh: 15000) |
| bank | String | Nama aplikasi keuangan (Contoh: "BCA", "Gopay") |
| hash | String | Signature MD5 hasil penggabungan nominal & Secret Code |
securityVerifikasi Signature MD5
Untuk memastikan request benar-benar dikirim secara sah oleh aplikasi EBayar di HP Anda, lakukan verifikasi kecocokan signature di backend server Anda:
hash_lokal = md5(amount + Secret_Code)- Gabungkan nominal amount dengan Secret Code Anda tanpa pemisah/spasi.
- Hitung hash MD5 dari teks gabungan tersebut.
- Bandingkan hash_lokal dengan parameter hash yang dikirim di payload JSON. Jika cocok, transaksi terverifikasi sah!
Contoh Kode Integrasi
Ikuti 3 langkah mudah ini untuk membangun sistem QRIS dinamis di website Anda dari awal sampai akhir.
Langkah 1: Ekstrak QRIS Statis (Frontend)
Gunakan library jsQR untuk memindai gambar QRIS dari akrilik toko Anda yang di-upload atau di-drag oleh user.
<!-- 1. Sertakan library jsQR dari CDN -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jsQR.min.js"></script>
<input type="file" id="qr-upload" accept="image/*" />
<p id="qris-result" style="color: white;">Hasil: </p>
<script>
document.getElementById('qr-upload').addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(event) {
const img = new Image();
img.onload = function() {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, img.width, img.height);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// 2. Decode QRIS
const code = jsQR(imageData.data, imageData.width, imageData.height);
if (code) {
document.getElementById('qris-result').innerText = "Hasil: " + code.data;
// Kirim code.data (string QRIS Statis) ke server Anda (Langkah 2)
} else {
alert("QR tidak terdeteksi!");
}
};
img.src = event.target.result;
};
reader.readAsDataURL(file);
});
</script>Langkah 2: Generate QRIS Dinamis (Backend)
Ubah string QRIS Statis menjadi Dinamis dengan menambahkan nominal pada Tag 54 dan menghitung ulang CRC16.
<?php
function calculateCRC16($str) {
$crc = 0xFFFF;
for ($c = 0; $c < strlen($str); $c++) {
$crc ^= ord($str[$c]) << 8;
for ($i = 0; $i < 8; $i++) {
if ($crc & 0x8000) {
$crc = ($crc << 1) ^ 0x1021;
} else {
$crc = $crc << 1;
}
}
}
$hex = strtoupper(dechex($crc & 0xFFFF));
return str_pad($hex, 4, '0', STR_PAD_LEFT);
}
function generateDynamicQR($qrisStatis, $nominal) {
$qris = substr($qrisStatis, 0, -4);
$qris = str_replace("010211", "010212", $qris);
$amountStr = (string)$nominal;
$len = str_pad(strlen($amountStr), 2, '0', STR_PAD_LEFT);
$tag54 = "54" . $len . $amountStr;
if (strpos($qris, "5802ID") !== false) {
$qris = str_replace("5802ID", $tag54 . "5802ID", $qris);
} else {
$qris = str_replace("6304", $tag54 . "6304", $qris);
}
return $qris . calculateCRC16($qris);
}
// Tangkap JSON dari fetch API Frontend
$jsonBody = file_get_contents('php://input');
$data = json_decode($jsonBody, true);
if (isset($data['qrisStatis']) && isset($data['nominal'])) {
$qrisDinamis = generateDynamicQR($data['qrisStatis'], $data['nominal']);
header('Content-Type: application/json');
echo json_encode(["status" => "success", "qrisDinamis" => $qrisDinamis]);
}
?><?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class QrisController {
public static function calculateCRC16($str) {
$crc = 0xFFFF;
for ($c = 0; $c < strlen($str); $c++) {
$crc ^= ord($str[$c]) << 8;
for ($i = 0; $i < 8; $i++) {
if ($crc & 0x8000) {
$crc = ($crc << 1) ^ 0x1021;
} else {
$crc = $crc << 1;
}
}
}
return str_pad(strtoupper(dechex($crc & 0xFFFF)), 4, '0', STR_PAD_LEFT);
}
public static function generateDynamicQR($qrisStatis, $nominal) {
$qris = substr($qrisStatis, 0, -4);
$qris = str_replace("010211", "010212", $qris);
$amountStr = (string)$nominal;
$tag54 = "54" . str_pad(strlen($amountStr), 2, '0', STR_PAD_LEFT) . $amountStr;
$qris = str_replace(["5802ID", "6304"], [$tag54."5802ID", $tag54."6304"], $qris);
return $qris . self::calculateCRC16($qris);
}
// Endpoint API
public function generate(Request $request) {
$qrisStatis = $request->input('qrisStatis');
$nominal = $request->input('nominal');
$qrisDinamis = self::generateDynamicQR($qrisStatis, $nominal);
return response()->json(["status" => "success", "qrisDinamis" => $qrisDinamis]);
}
}
?>function calculateCRC16(str) {
let crc = 0xFFFF;
for (let c = 0; c < str.length; c++) {
crc ^= str.charCodeAt(c) << 8;
for (let i = 0; i < 8; i++) {
if (crc & 0x8000) {
crc = (crc << 1) ^ 0x1021;
} else {
crc = crc << 1;
}
}
}
return (crc & 0xFFFF).toString(16).toUpperCase().padStart(4, '0');
}
function generateDynamicQR(qrisStatis, nominal) {
let qris = qrisStatis.slice(0, -4);
qris = qris.replace("010211", "010212");
const amtStr = nominal.toString();
const len = amtStr.length.toString().padStart(2, '0');
const tag54 = `54${len}${amtStr}`;
if (qris.includes("5802ID")) {
qris = qris.replace("5802ID", tag54 + "5802ID");
} else {
qris = qris.replace("6304", tag54 + "6304");
}
return qris + calculateCRC16(qris);
}
// Endpoint API Express.js
app.post('/api/generate-qris', (req, res) => {
const { qrisStatis, nominal } = req.body;
if (!qrisStatis || !nominal) {
return res.status(400).json({ error: "Data tidak lengkap" });
}
const qrisDinamis = generateDynamicQR(qrisStatis, nominal);
res.json({ status: "success", qrisDinamis });
});
def calculate_crc16(data: str) -> str:
crc = 0xFFFF
for char in data:
crc ^= ord(char) << 8
for _ in range(8):
if crc & 0x8000:
crc = (crc << 1) ^ 0x1021
else:
crc = crc << 1
crc &= 0xFFFF
return f"{crc:04X}"
def generate_dynamic_qr(qris_statis: str, nominal: int) -> str:
qris = qris_statis[:-4]
qris = qris.replace("010211", "010212")
amt_str = str(nominal)
length = str(len(amt_str)).zfill(2)
tag54 = f"54{length}{amt_str}"
if "5802ID" in qris:
qris = qris.replace("5802ID", tag54 + "5802ID")
else:
qris = qris.replace("6304", tag54 + "6304")
return qris + calculate_crc16(qris)
# Endpoint API Flask
@app.route('/api/generate-qris', methods=['POST'])
def generate_qris_endpoint():
data = request.json
qris_statis = data.get('qrisStatis')
nominal = data.get('nominal')
if not qris_statis or not nominal:
return jsonify({"error": "Data tidak lengkap"}), 400
qris_dinamis = generate_dynamic_qr(qris_statis, nominal)
return jsonify({"status": "success", "qrisDinamis": qris_dinamis})
package main
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
)
func calculateCRC16(str string) string {
crc := 0xFFFF
for i := 0; i < len(str); i++ {
crc ^= int(str[i]) << 8
for j := 0; j < 8; j++ {
if (crc & 0x8000) != 0 {
crc = (crc << 1) ^ 0x1021
} else {
crc = crc << 1
}
}
}
crc &= 0xFFFF
return fmt.Sprintf("%04X", crc)
}
func generateDynamicQR(qrisStatis string, nominal int) string {
qris := qrisStatis[:len(qrisStatis)-4]
qris = strings.Replace(qris, "010211", "010212", 1)
amtStr := strconv.Itoa(nominal)
length := fmt.Sprintf("%02d", len(amtStr))
tag54 := "54" + length + amtStr
if strings.Contains(qris, "5802ID") {
qris = strings.Replace(qris, "5802ID", tag54+"5802ID", 1)
} else {
qris = strings.Replace(qris, "6304", tag54+"6304", 1)
}
return qris + calculateCRC16(qris)
}
// API Handler Golang
type QrisRequest struct {
QrisStatis string `json:"qrisStatis"`
Nominal int `json:"nominal"`
}
func generateHandler(w http.ResponseWriter, r *http.Request) {
var req QrisRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
qrisDinamis := generateDynamicQR(req.QrisStatis, req.Nominal)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"status": "success",
"qrisDinamis": qrisDinamis,
})
}
public class QrisHelper {
public static String calculateCRC16(String str) {
int crc = 0xFFFF;
for (int i = 0; i < str.length(); i++) {
crc ^= (str.charAt(i) << 8);
for (int j = 0; j < 8; j++) {
if ((crc & 0x8000) != 0) {
crc = (crc << 1) ^ 0x1021;
} else {
crc = crc << 1;
}
}
}
crc &= 0xFFFF;
return String.format("%04X", crc);
}
public static String generateDynamicQR(String qrisStatis, int nominal) {
String qris = qrisStatis.substring(0, qrisStatis.length() - 4);
qris = qris.replace("010211", "010212");
String amtStr = String.valueOf(nominal);
String len = String.format("%02d", amtStr.length());
String tag54 = "54" + len + amtStr;
if (qris.contains("5802ID")) {
qris = qris.replace("5802ID", tag54 + "5802ID");
} else {
qris = qris.replace("6304", tag54 + "6304");
}
return qris + calculateCRC16(qris);
}
}
// Endpoint API Spring Boot
@RestController
@RequestMapping("/api")
public class QrisController {
@PostMapping("/generate-qris")
public ResponseEntity<Map<String, String>> generate(@RequestBody Map<String, Object> payload) {
String qrisStatis = (String) payload.get("qrisStatis");
Integer nominal = (Integer) payload.get("nominal");
String qrisDinamis = QrisHelper.generateDynamicQR(qrisStatis, nominal);
Map<String, String> response = new HashMap<>();
response.put("status", "success");
response.put("qrisDinamis", qrisDinamis);
return ResponseEntity.ok(response);
}
}
Langkah 3: Webhook Verifikasi Pembayaran (Backend)
Gunakan contoh kode backend berikut untuk memverifikasi signature MD5 dan menandai status transaksi lunas di website toko Anda secara instan.
infoSyarat & Penanganan Tes Koneksi Webhook (Ping Test)
Saat Anda menyimpan atau menguji URL Webhook di aplikasi HP EBayar, sistem akan mengirim POST request tes koneksi berisi payload: { "amount": 0, "hash": "...", "is_test": true }.
- Wajib Respons HTTP 200 OK: Server backend Anda harus merespon request tes tersebut dengan HTTP status code
200(misal:res.status(200).json({ status: 'success' })). - Hindari Respon HTTP 404 pada Tes: Jangan mengembalikan status
404 Not Foundjika invoice dengan nominal 0 tidak ditemukan di DB. Respon 404 akan menyebabkan aplikasi EBayar menampilkan pesan error "Gagal (404): Server offline atau URL tidak ditemukan". - URL Harus Lengkap: Pastikan memasukkan URL lengkap ke endpoint handler (contoh:
https://domain.com/api/webhook/payment), bukan hanya domain/folder utamanya saja.
<?php
// 1. Tentukan secret code yang sama dengan di aplikasi HP
$secretCode = "RAHASIA_KASIR_123";
// 2. Tangkap raw JSON payload dari EBayar
$jsonBody = file_get_contents('php://input');
$data = json_decode($jsonBody, true) ?? [];
$amount = $data['amount'] ?? 0;
$hash = $data['hash'] ?? '';
$bank = $data['bank'] ?? '';
$isTest = $data['is_test'] ?? false;
// 3. Tangani tes koneksi (ping) dari EBayar
if ($isTest || $amount === 0) {
http_response_code(200);
echo json_encode(["status" => "success", "message" => "Webhook test received"]);
exit;
}
// 4. Hitung MD5 lokal
$myHash = md5($amount . $secretCode);
// 5. Cocokkan hash
if ($hash === $myHash) {
// --- SIGNATURE VALID ---
// Cari invoice pending dengan nominal yang cocok di DB Anda, lalu ubah status menjadi PAID
echo json_encode(["status" => "success", "message" => "Payment verified"]);
} else {
// Request tidak sah
http_response_code(401);
echo json_encode(["status" => "error", "message" => "Invalid Signature"]);
}
?><?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Invoice;
class WebhookController extends Controller
{
public function handleEbayar(Request $request)
{
$secretCode = env('EBAYAR_SECRET_CODE', 'RAHASIA_KASIR_123');
$amount = $request->input('amount', 0);
$hash = $request->input('hash', '');
$bank = $request->input('bank', '');
$isTest = $request->input('is_test', false);
// Tangani tes koneksi (ping) dari EBayar
if ($isTest || $amount === 0) {
return response()->json(['status' => 'success', 'message' => 'Webhook test received'], 200);
}
// Hitung MD5 lokal
$myHash = md5($amount . $secretCode);
if ($hash === $myHash) {
// --- SIGNATURE VALID ---
$invoice = Invoice::where('amount', $amount)
->where('status', 'PENDING')
->first();
if ($invoice) {
$invoice->update([
'status' => 'PAID',
'bank_penerima' => $bank
]);
}
return response()->json(['status' => 'success', 'message' => 'Payment verified']);
}
return response()->json(['status' => 'error', 'message' => 'Invalid Signature'], 401);
}
}
?>const express = require('express');
const crypto = require('crypto');
const app = express();
// Gunakan JSON middleware parser
app.use(express.json());
app.post('/webhook', (req, res) => {
const secretCode = "RAHASIA_KASIR_123";
const { amount, hash, bank, is_test } = req.body;
// Tangani tes koneksi (ping) dari EBayar
if (is_test || amount === 0) {
return res.status(200).json({ status: 'success', message: 'Webhook test received' });
}
// Hitung MD5 lokal
const myHash = crypto
.createHash('md5')
.update(amount.toString() + secretCode)
.digest('hex');
if (hash === myHash) {
// --- SIGNATURE VALID ---
// Update status transaksi di DB toko menjadi Lunas (PAID)
res.json({ status: 'success', message: 'Payment verified' });
} else {
res.status(401).json({ status: 'error', message: 'Invalid Signature' });
}
});from flask import Flask, request, jsonify
import hashlib
app = Flask(__name__)
@app.route('/webhook', methods=['POST'])
def webhook():
secret_code = "RAHASIA_KASIR_123"
data = request.get_json() or {}
amount = data.get('amount', 0)
received_hash = data.get('hash', '')
bank = data.get('bank', '')
is_test = data.get('is_test', False)
# Tangani tes koneksi (ping) dari EBayar
if is_test or amount == 0:
return jsonify({"status": "success", "message": "Webhook test received"}), 200
# Hitung MD5 lokal
raw_str = f"{amount}{secret_code}"
my_hash = hashlib.md5(raw_str.encode('utf-8')).hexdigest()
if received_hash == my_hash:
return jsonify({"status": "success", "message": "Payment verified"}), 200
else:
return jsonify({"status": "error", "message": "Invalid Signature"}), 401
if __name__ == '__main__':
app.run(port=5000)package main
import (
"crypto/md5"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
)
const secretCode = "RAHASIA_KASIR_123"
type WebhookPayload struct {
Amount int `json:"amount"`
Bank string `json:"bank"`
Hash string `json:"hash"`
IsTest bool `json:"is_test"`
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method not allowed", 405)
return
}
body, _ := io.ReadAll(r.Body)
defer r.Body.Close()
var payload WebhookPayload
if err := json.Unmarshal(body, &payload); err != nil {
http.Error(w, "Invalid JSON", 400)
return
}
w.Header().Set("Content-Type", "application/json")
// Tangani tes koneksi (ping) dari EBayar
if payload.IsTest || payload.Amount == 0 {
json.NewEncoder(w).Encode(map[string]string{
"status": "success",
"message": "Webhook test received",
})
return
}
raw := fmt.Sprintf("%d%s", payload.Amount, secretCode)
myHash := fmt.Sprintf("%x", md5.Sum([]byte(raw)))
if payload.Hash == myHash {
json.NewEncoder(w).Encode(map[string]string{
"status": "success",
"message": "Payment verified",
})
} else {
w.WriteHeader(401)
json.NewEncoder(w).Encode(map[string]string{
"status": "error",
"message": "Invalid Signature",
})
}
}
func main() {
http.HandleFunc("/webhook", webhookHandler)
log.Println("Server started on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}package com.ebayar.webhook;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import java.security.MessageDigest;
import java.util.Map;
@RestController
public class WebhookController {
private static final String SECRET_CODE = "RAHASIA_KASIR_123";
@PostMapping("/webhook")
public ResponseEntity<Map<String, String>> handleWebhook(
@RequestBody Map<String, Object> payload) {
// 1. Ambil data dari payload
int amount = (int) payload.getOrDefault("amount", 0);
String hash = (String) payload.getOrDefault("hash", "");
String bank = (String) payload.getOrDefault("bank", "");
boolean isTest = (boolean) payload.getOrDefault("is_test", false);
// 2. Tangani tes koneksi (ping) dari EBayar
if (isTest || amount == 0) {
return ResponseEntity.ok(Map.of(
"status", "success",
"message", "Webhook test received"
));
}
// 3. Hitung MD5 lokal: md5(amount + secretCode)
String myHash = md5(amount + SECRET_CODE);
// 4. Cocokkan hash
if (hash.equals(myHash)) {
// --- SIGNATURE VALID ---
// Cari invoice pending di DB, misalnya:
// Invoice inv = invoiceRepo.findByAmountAndStatus(amount, "PENDING");
//
// Jika ditemukan, ubah status menjadi Lunas (PAID):
// inv.setStatus("PAID");
// inv.setBankPenerima(bank);
// invoiceRepo.save(inv);
return ResponseEntity.ok(Map.of(
"status", "success",
"message", "Payment verified"
));
} else {
return ResponseEntity.status(401).body(Map.of(
"status", "error",
"message", "Invalid Signature"
));
}
}
// Helper method untuk menghitung MD5
private String md5(String input) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(input.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte b : digest) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (Exception e) {
throw new RuntimeException("MD5 hashing failed", e);
}
}
}