# RisaPay API - Panduan Lengkap untuk AI & Developer
> Satu file, seluruh alur API RisaPay. Base URL: `https://risapay.xfazrin.my.id`
> Terakhir diperbarui: September 2026. Bahasa: Indonesia.

## 1. Gambaran alur (wajib paham dulu)

```
[Server merchant] --1. POST api/transaction/create--> [RisaPay] --> [Provider: Duitku/Xendit/iPaymu]
[Server merchant] <--2. JSON { success:true, data:{ reference, qr_string/pay_code/pay_url, status:UNPAID } }
[Pelanggan] bayar via QR / VA / e-wallet / Alfamart (atau buka {base}checkout/?page={reference})
[Provider] --3. callback--> [RisaPay: saldo merchant += amount_received, status=PAID]
[RisaPay] --4. POST JSON ke callback_url merchant--> [Server merchant: verifikasi signature, tandai lunas, jawab 200]
[Server merchant] -opsional- GET api/transaction/detail?reference=... (rekonsiliasi)
```

Khusus OVO ada langkah tambahan (bagian 5).

## 2. Kredensial

- `api_key`: header `Authorization: Bearer <api_key>`. Lokasi: dashboard > Pengaturan. Bisa di-regenerate (key lama langsung mati).
- `merchant_code`: kode merchant, contoh `R39`. Lokasi: dashboard > Merchant. Satu user bisa punya banyak merchant.
- `private_key`: dipakai untuk signature & verifikasi callback. Lokasi: dashboard > Pengaturan. Rahasia, hanya di server.

## 3. Signature request (create transaction)

```
signature = HMAC-SHA256( merchant_code + merchant_ref + amount , private_key )
```

- Ketiga nilai adalah **string mentah persis seperti yang dikirim** (hanya di-trim). `amount=10000` → string `"10000"`.
- Output hex 64 karakter.
- PHP: `$signature = hash_hmac('sha256', $merchantCode . $merchantRef . $amount, $privateKey);`
- Node.js: `crypto.createHmac('sha256', privateKey).update(merchantCode + merchantRef + amount).digest('hex')`
- RisaPay match signature ke SEMUA merchant milik user buat nemuin `merchant_code` yang kepake. Signature salah = `Invalid signature`.

## 4. Endpoint 1 - Buat Transaksi

```
POST https://risapay.xfazrin.my.id/api/transaction/create
Header: Authorization: Bearer <api_key>
        Content-Type: application/x-www-form-urlencoded
Body: form-encoded (BUKAN JSON)
```

### Parameter

| Nama | Wajib | Keterangan |
|---|---|---|
| `method` | Ya | Kode channel (huruf besar). Contoh: `QRIS`, `BRIVA`, `DANA`, `OVO`, `SHOPEEPAY`, `LINKAJA`, `ASTRAPAY`, `ALFAMART`, `MYBVA`, `PERMATAVA`, `BNIVA`, `BCAVA`, `MANDIRIVA`, `CIMBVA`, `BNCVA`, `AGVA`, `ATMBVA`, `QRIS2`, `QRISC`, `QRISI`, `QRIS2X`, `QRISxendit`, `QRIS_SHOPEEPAY` |
| `merchant_ref` | Ya | Referensi unik versi merchant, unik per merchant. Duplikat ditolak + diberi tahu ref & status bentrok |
| `amount` | Ya | Nominal rupiah (angka > 0), harus dalam batas min/max channel |
| `signature` | Ya | HMAC bagian 3 |
| `customer_name` | Tidak | Nama pelanggan |
| `customer_email` | Tidak | Default `email@example.com` bila kosong |
| `customer_phone` | Tidak | No. HP pelanggan |
| `return_url` | Tidak | Redirect setelah bayar. Default `https://example.com/` |
| `callback_url` | Tidak | Override callback per-transaksi. Default = callback_url merchant |
| `expired_time` | Tidak | Unix timestamp. Default: VA 24 jam, lainnya 48 jam dari sekarang |
| `order_items` | Tidak | Rincian item (price × quantity dihitung jadi subtotal) |

### Contoh request (cURL)

```bash
curl -X POST "https://risapay.xfazrin.my.id/api/transaction/create" \
  -H "Authorization: Bearer API_KEY_KAMU" \
  --data-urlencode "method=QRIS" \
  --data-urlencode "merchant_ref=INV-2026-0001" \
  --data-urlencode "amount=50000" \
  --data-urlencode "customer_name=Budi" \
  --data-urlencode "customer_email=budi@mail.com" \
  --data-urlencode "customer_phone=081234567890" \
  --data-urlencode "signature=HASH_HMAC_KAMU"
```

### Contoh request (PHP)

```php
$merchantCode = 'R39';
$merchantRef  = 'INV-2026-0001';
$amount       = '50000'; // string, sama persis dengan yang dikirim
$apiKey       = 'API_KEY_KAMU';
$privateKey   = 'PRIVATE_KEY_KAMU';

$signature = hash_hmac('sha256', $merchantCode . $merchantRef . $amount, $privateKey);

$ch = curl_init('https://risapay.xfazrin.my.id/api/transaction/create');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
    CURLOPT_POSTFIELDS => http_build_query([
        'method' => 'QRIS', 'merchant_ref' => $merchantRef, 'amount' => $amount,
        'customer_name' => 'Budi', 'customer_email' => 'budi@mail.com',
        'customer_phone' => '081234567890', 'signature' => $signature,
    ]),
]);
$res = json_decode(curl_exec($ch), true);
curl_close($ch);

if (!empty($res['success'])) {
    $reference = $res['data']['reference'];   // simpan! kunci semua proses berikut
    $qrString  = $res['data']['qr_string'];   // tampilkan sebagai QR ke pelanggan
    // VA: pakai $res['data']['pay_code']; e-wallet: arahkan ke $res['data']['pay_url'];
    // atau arahkan pelanggan ke: https://risapay.xfazrin.my.id/checkout/?page=' . $reference
}
```

### Contoh request (Node.js)

```js
const crypto = require('crypto');
const merchantCode = 'R39', merchantRef = 'INV-2026-0001', amount = '50000';
const signature = crypto.createHmac('sha256', process.env.RISAPAY_PRIVATE_KEY)
  .update(merchantCode + merchantRef + amount).digest('hex');

const body = new URLSearchParams({ method: 'QRIS', merchant_ref: merchantRef,
  amount, customer_name: 'Budi', signature });
const r = await fetch('https://risapay.xfazrin.my.id/api/transaction/create', {
  method: 'POST',
  headers: { 'Authorization': 'Bearer ' + process.env.RISAPAY_API_KEY },
  body });
const res = await r.json();
if (res.success) console.log(res.data.reference, res.data.qr_string);
```

### Respons VALID - QRIS (HTTP 200)

```json
{
    "success": true,
    "message": "Successfully generate transaction",
    "data": {
        "reference": "R1700000000XXXXX",
        "merchant_ref": "INV-2026-0001",
        "payment_selection_type": "static",
        "payment_method": "QRIS",
        "payment_name": "QRIS",
        "customer_name": "Budi",
        "customer_email": "budi@mail.com",
        "customer_phone": "081234567890",
        "callback_url": "https://toko.id/callback",
        "return_url": "https://toko.id/thanks",
        "amount": 50000,
        "fee_merchant": 70,
        "fee_customer": 0,
        "total_fee": 70,
        "amount_received": 49930,
        "pay_code": null,
        "pay_url": null,
        "checkout_url": null,
        "status": "UNPAID",
        "expired_time": 1700172800,
        "order_items": null,
        "instructions": [{ "title": "Pembayaran via QRIS", "steps": ["..."] }],
        "qr_string": "00020101...",
        "qr_url": null
    }
}
```

Artinya: `reference` = ID RisaPay (simpan di DB merchant, dipakai untuk cek status & halaman bayar).
`qr_string` = render jadi QR. VA = `pay_code` (nomor VA). E-wallet = `pay_url` (redirect).
`amount_received` = saldo yang akan masuk ke merchant. `status` selalu `UNPAID` saat dibuat.
Halaman bayar siap pakai: `https://risapay.xfazrin.my.id/checkout/?page={reference}`.

### Respons VALID - Virtual Account (HTTP 200, bedanya)

```json
{
    "success": true,
    "message": "Successfully generate transaction",
    "data": {
        "reference": "R1700000000XXXXX",
        "merchant_ref": "INV-2026-0001",
        "payment_method": "BRIVA",
        "payment_name": "BRI Virtual Account",
        "amount": 50000,
        "fee_merchant": 3250,
        "fee_customer": 0,
        "total_fee": 4000,
        "amount_received": 46750,
        "pay_code": "88810123456789",
        "status": "UNPAID",
        "expired_time": 1700086400
    }
}
```

### Respons GAGAL (HTTP 200, `success: false` - kecuali rate limit 429)

```json
{ "success": false, "message": "Invalid signature" }
```

Daftar lengkap `message` gagal + cara betulkan: lihat bagian 8 (tabel error).

## 5. Endpoint 2 - Cek Status Transaksi

```
GET https://risapay.xfazrin.my.id/api/transaction/detail?reference={reference}
Header: Authorization: Bearer <api_key>
```

Cuma transaksi milik pemilik `api_key` yang kebaca. Pas buat polling & rekon.

### Respons VALID (HTTP 200)

```json
{
    "success": true,
    "message": "Transaction found",
    "data": {
        "reference": "R1700000000XXXXX",
        "merchant_ref": "INV-2026-0001",
        "payment_selection_type": "static",
        "payment_method": "QRIS",
        "payment_name": "QRIS",
        "customer_name": "Budi",
        "customer_email": "budi@mail.com",
        "customer_phone": "081234567890",
        "callback_url": "https://toko.id/callback",
        "return_url": "https://toko.id/thanks",
        "amount": "10000",
        "fee_merchant": "70",
        "fee_customer": "0",
        "total_fee": "70",
        "amount_received": "9930",
        "pay_code": "88810...",
        "pay_url": "https://...",
        "checkout_url": "https://...",
        "status": "UNPAID",
        "paid_at": null,
        "expired_time": "1700172800",
        "order_items": [],
        "qr_string": "00020101...",
        "qr_url": null
    }
}
```

Catatan: `status` salah satu dari `UNPAID`, `PAID`, `EXPIRED`, `FAILED`, `REFUND`.
`paid_at` = unix timestamp pas lunas, `null` kalau belum bayar. `checkout_url` itu alias dari `pay_url`.

### Respons GAGAL

```json
{ "success": false, "message": "Transaction not found" }
{ "success": false, "message": "Invalid API Key" }
{ "success": false, "message": "Header Authorization tidak ada dalam permintaan" }
{ "success": false, "message": "Invalid Header Authorization/tidak sesuai format" }
{ "success": false, "message": "Maintenance" }
```

## 6. Endpoint 3 - Push OVO (2 langkah, khusus OVO)

Langkah 1: buat transaksi seperti biasa dengan `method=OVO` → dapat `reference`.
Langkah 2:

```
POST https://risapay.xfazrin.my.id/api/ovo-payment/process.php
Body (form): reference={reference dari langkah 1} & ovo_number={no HP OVO, cth 0812xxxx}
Tanpa Authorization.
```

### Respons VALID

```json
{ "status": "PAID", "message": "Periksa aplikasi OVO Anda" }
```

Pelanggan menyetujui tagihan di aplikasi OVO-nya. Callback PAID tetap dikirim seperti biasa.

### Respons GAGAL

```json
{ "status": "GAGAL", "message": "Gagal update ke database" }
{ "status": "GAGAL", "message": "Server Error 500 <pesan provider>" }
```

## 7. Callback ke merchant (WAJIB diimplementasikan)

Pas pelanggan lunas, RisaPay `POST` JSON ke `callback_url`:

```
POST {callback_url}
Content-Type: application/json
X-Callback-Signature: <hex HMAC-SHA256 dari RAW BODY dengan private_key>
X-Callback-Event: payment_status
```

### Payload (14 field, selalu bentuk ini)

```json
{
    "reference": "R1715094140S41FP",
    "merchant_ref": "Q3123660",
    "payment_method": "QRIS",
    "payment_method_code": "QRIS",
    "total_amount": 117,
    "fee_merchant": 0,
    "fee_customer": 0,
    "total_fee": 0,
    "amount_received": 117,
    "is_closed_payment": 1,
    "status": "PAID",
    "paid_at": 1715094192,
    "note": "Transaksi sukses"
}
```

`status` selalu `"PAID"`, `paid_at` unix timestamp, `is_closed_payment` selalu `1`.

### Aturan main

1. **Verifikasi signature dari RAW body** (jangan parse-encode ulang - urutan key/spasi bisa beda).
2. **Cek `merchant_ref` + nominal** cocok dengan pesananmu, lalu tandai lunas.
3. **Idempoten**: callback yang sama BISA double - pakai `reference` sebagai key, skip yang sudah paid.
4. **Wajib balas HTTP 200 secepatnya**. Selain 200 = fail → RisaPay retry otomatis maks 3x (timeout 10 detik/percobaan).
5. `callback_url` boleh di-override per-transaksi via parameter `callback_url` saat create.

### Verifikasi (PHP)

```php
$raw = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_CALLBACK_SIGNATURE'] ?? '';
$calc = hash_hmac('sha256', $raw, $privateKeyAnda);
if (!hash_equals($calc, $sig)) { http_response_code(403); exit; }
$data = json_decode($raw, true);
// cocokkan $data['merchant_ref'] + $data['total_amount'] dengan pesanan,
// tandai lunas berdasar $data['reference'] (idempoten!), lalu:
http_response_code(200);
```

### Verifikasi (Node.js/Express)

```js
const raw = req.body; // Buffer - butuh express.raw({ type: 'application/json' })
const sig = req.headers['x-callback-signature'] || '';
const calc = crypto.createHmac('sha256', process.env.RISAPAY_PRIVATE_KEY)
  .update(raw).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(calc), Buffer.from(sig))) return res.sendStatus(403);
const data = JSON.parse(raw.toString());
// ... cocokkan merchant_ref + total_amount, tandai lunas per reference (idempoten)
res.sendStatus(200);
```

## 8. Tabel error create transaction (lengkap)

| `message` | HTTP | Penyebab & cara betulkan |
|---|---|---|
| `Maintenance` | 200 | Server maintenance, coba lagi nanti |
| `Header Authorization tidak ada dalam permintaan` | 200 | Header `Authorization` tidak terkirim (cek reverse proxy teruskan header) |
| `Invalid Header Authorization/tidak sesuai format` | 200 | Format harus persis `Bearer <api_key>` (satu spasi) |
| `Rate limit terlampaui. Coba lagi sebentar.` | **429** | > 60 request/menit/IP - turunkan frekuensi / cache hasil detail |
| `Invalid API Key` | 200 | `api_key` salah / sudah di-regenerate - ambil ulang di dashboard |
| `Invalid parameter permintaan` | 200 | Keempatnya hilang sekaligus: method, merchant_ref, amount, signature |
| `method tidak ada dalam permintaan` | 200 | Parameter `method` tidak dikirim |
| `merchant_ref tidak ada dalam permintaan` | 200 | Parameter `merchant_ref` tidak dikirim |
| `amount tidak ada dalam permintaan` | 200 | Parameter `amount` tidak dikirim |
| `signature tidak ada dalam permintaan` | 200 | Parameter `signature` tidak dikirim |
| `Invalid method` | 200 | Kode channel tidak dikenal / file channel tidak ada - cek ejaan |
| `Invalid signature` | 200 | Rumus/kunci salah - cek urutan `merchant_code+merchant_ref+amount` (string mentah!), `private_key`, dan pastikan `merchant_code` milikmu |
| `merchant_ref sudah digunakan (ref: ..., status: ...). Gunakan merchant_ref lain.` | 200 | Duplikat - pakai merchant_ref baru yang unik |
| `Invalid amount` | 200 | Bukan angka / <= 0 |
| `Method tidak aktif` | 200 | Channel sedang dimatikan admin - pakai channel lain |
| `Minimum payment amount is <min>` | 200 | Nominal di bawah minimum channel - naikkan amount |
| `Maximum payment amount is Rp <max>` | 200 | Nominal di atas maksimum channel - turunkan / pecah transaksi |
| `Server Error <kode> <pesan>` | 200 | Provider hulu error - coba lagi / ganti channel |
| `Error database, mohon melaporkan pada Admin` | 200 | Gangguan internal - hubungi RisaPay |

Semua logic error = HTTP 200 + `{"success": false, "message": "..."}`. Yang 429 cuma rate limit.

## 9. Status & masa berlaku

- `UNPAID` (baru dibuat) → `PAID` (lunas via callback) / `EXPIRED` (lewat `expired_time`) / `FAILED` / `REFUND`.
- `expired_time` default: VA 24 jam, QRIS/e-wallet/retail 48 jam. Bisa di-set manual (unix timestamp).
- Jangan mark paid dari return page, cuma dari verified callback atau `detail` = `PAID`.

## 10. Daftar kode `method`

QRIS: `QRIS`, `QRIS2`, `QRISC`, `QRISI`, `QRIS2X`, `QRISxendit`, `QRIS_SHOPEEPAY`.
E-wallet: `DANA`, `OVO` (+ endpoint push bagian 6), `SHOPEEPAY`, `LINKAJA`, `ASTRAPAY`.
Virtual Account: `MYBVA`, `PERMATAVA`, `BNIVA`, `BRIVA`, `MANDIRIVA`, `BCAVA`, `CIMBVA`, `BNCVA`, `AGVA`, `ATMBVA`.
(Catatan: file `ALFAMART.php` ada tapi channel-nya belum terdaftar di database, jadi `method=ALFAMART` untuk sekarang dijawab `Invalid method`.)
(Min/max/fee tiap channel tampil live di halaman developer, bisa berubah sewaktu-waktu oleh admin.)

## 11. Checklist integrasi (copy ke AI-mu)

1. Ambil `api_key`, `merchant_code`, `private_key` dari dashboard RisaPay.
2. Buat transaksi (bagian 4) → simpan `reference` + tampilkan QR/VA/link ke pelanggan.
3. Sediakan endpoint callback (bagian 7): verifikasi signature, cocokkan nominal, idempoten, jawab 200.
4. Rekonsiliasi berkala via detail (bagian 5). OVO pakai 2 langkah (bagian 6).
5. Tangani semua error bagian 8 dengan pesan yang ramah di UI-mu.
