Dokumentasi Integrasi

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:

FieldTipeKeterangan
amountIntegerNominal uang masuk (Contoh: 15000)
bankStringNama aplikasi keuangan (Contoh: "BCA", "Gopay")
hashStringSignature 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:

Rumus Tanda Tanganhash_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 Found jika 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);
        }
    }
}