Callback
Parameter dan struktur notifikasi callback.
Callback
Ketika pembeli berhasil melakukan pembayaran, iPaymu akan mengirimkan notifikasi callback ke endpoint yang telah Anda konfigurasi. Dokumen ini menjelaskan struktur lengkap callback, header yang diperlukan, dan proses validasi signature.
Konfigurasi
Anda dapat mengatur tipe konten callback di dashboard iPaymu. Pilih format yang sesuai dengan sistem Anda:
| Content-Type | Deskripsi |
|---|---|
application/x-www-form-urlencoded | Default. Direkomendasikan untuk stabilitas dan kompatibilitas yang lebih baik |
application/json | Format alternatif jika sistem Anda lebih menyukai JSON |
Link Pengaturan:
- Production: https://my.ipaymu.com/integration/setting
- Sandbox: https://sandbox.ipaymu.com/integration/setting
Secret Key untuk Validasi Signature
Untuk melakukan validasi signature pada callback, gunakan Nomor VA (Virtual Account) akun iPaymu Anda sebagai secret key.
Cara mendapatkan Secret Key:
- Login ke Dashboard iPaymu
- Pilih menu Integration atau Pengaturan
- Klik API Key atau Kunci API
- Nomor VA Anda akan terlihat di bagian API Key atau Merchant VA
Contoh Secret Key: 1179001234567890
⚠️ Penting: Jangan pernah membagikan Nomor VA Anda atau menyimpannya di repositori publik. Gunakan environment variables untuk menyimpan secret key di production.
Alur Proses Callback
Berikut diagram alur lengkap proses callback dari iPaymu:
Catatan Penting:
- Jika signature tidak valid, jangan proses transaksi
- Selalu response dengan HTTP 200 untuk menghentikan retry
- Implementasikan idempotency untuk menghindari duplikasi
Catatan Penting
Sebelum mengimplementasikan penanganan callback, pastikan Anda memahami poin-poin berikut:
| Topik | Deskripsi |
|---|---|
| Mekanisme Retry | iPaymu akan secara otomatis melakukan retry callback jika server Anda tidak merespons dengan 200 OK. Siapkan penanganan idempotensi yang tepat untuk menghindari duplikasi pemrosesan. |
| Idempotensi | Karena retry dapat terjadi, selalu periksa status transaksi di database sebelum memproses. Ini mencegah double-posting ketika callback yang sama datang berkali-kali. |
| Validasi Signature | Selalu validasi header X-Signature untuk memastikan request benar-benar berasal dari iPaymu. Jangan pernah memproses callback tanpa verifikasi. |
| Secret Key | Gunakan Nomor VA Merchant Anda sebagai secret key untuk validasi signature. |
Headers
Setiap request callback menyertakan headers berikut. X-Signature sangat penting untuk verifikasi keamanan.
| Header | Value | Deskripsi |
|---|---|---|
Accept | application/json | Tipe konten yang diharapkan iPaymu untuk response |
Content-Type | application/json | Format body request |
X-External-ID | 20251111101052209120 | Identifier unik untuk request callback ini |
X-Signature | c9594cc8... | Signature HMAC-SHA256 untuk validasi |
X-Timestamp | 2025-11-11T10:10:52+07:00 | Timestamp ketika callback dibuat |
Contoh Payload Lengkap
Berikut adalah contoh payload lengkap yang dikirimkan oleh iPaymu:
Contoh x-www-form-urlencoded
POST /callback HTTP/1.1
Host: your-domain.com
Content-Type: application/x-www-form-urlencoded
X-Signature: c9594cc8...
X-Timestamp: 2025-11-11T10:10:52+07:00
buyer_email=customer%40example.com&
buyer_name=John%20Doe&
buyer_phone=081234567890&
created_at=2025-11-11%2010%3A00%3A00&
expired_at=2025-11-11%2011%3A00%3A00&
fee=1500&
paid_at=2025-11-11%2010%3A10%3A52&
reference_id=ORDER123456&
sid=SESSION789&
status=berhasil&
status_code=1&
status_desc=Success&
channel=bca&
paid_off=98500&
product=Product%20Name&
quantity=1&
merchant=1179001234567890&
merchant_name=Your%20Store&
system_notes=Payment%20completed&
trscode=TRX789012&
trx_id=12345678&
unique_code=0&
via=va&
payment_no=1234567890&
va=1234567890&
url=https%3A%2F%2Fyour-domain.com%2Fcallback&
additional_info=%5B%5D&
transaction_status_code=1&
settlement_status=unsettle&
settlement_date=null&
expired_time=3600&
expired_unix=1762842000&
is_escrow=0&
is_refund=0&
is_sandbox=false&
total=100000&
amount=100000&
sub_total=100000&
referenceId=ORDER123456Contoh application/json
{
"buyer_email": "customer@example.com",
"buyer_name": "John Doe",
"buyer_phone": "081234567890",
"created_at": "2025-11-11 10:00:00",
"expired_at": "2025-11-11 11:00:00",
"fee": "1500",
"paid_at": "2025-11-11 10:10:52",
"reference_id": "ORDER123456",
"sid": "SESSION789",
"status": "berhasil",
"status_code": "1",
"status_desc": "Success",
"channel": "bca",
"paid_off": "98500",
"product": "Product Name",
"quantity": "1",
"merchant": "1179001234567890",
"merchant_name": "Your Store",
"system_notes": "Payment completed",
"trscode": "TRX789012",
"trx_id": "12345678",
"unique_code": "0",
"via": "va",
"payment_no": "1234567890",
"va": "1234567890",
"url": "https://your-domain.com/callback",
"additional_info": [],
"transaction_status_code": "1",
"settlement_status": "unsettle",
"settlement_date": null,
"expired_time": "3600",
"expired_unix": "1762842000",
"is_escrow": "0",
"is_refund": "0",
"is_sandbox": "false",
"total": "100000",
"amount": "100000",
"sub_total": "100000",
"referenceId": "ORDER123456"
}Struktur Data Callback
Tabel berikut menunjukkan semua kemungkinan field dalam response callback. Perhatikan dengan seksama tipe data — menggunakan tipe yang salah saat validasi signature akan menyebabkan ketidakcocokan hash.
Field Transaksi Inti
| Field | Tipe | Deskripsi | Catatan Validasi |
|---|---|---|---|
trx_id | Integer | ID transaksi unik di sistem iPaymu | Ubah dari String jika menerima via Form Data |
sid | String | Session ID transaksi | - |
reference_id | String | Reference ID merchant Anda | - |
status | String | Status teks: berhasil, pending, expired | - |
status_code | Integer | Kode status: 1 (Sukses), 0 (Pending), -2 (Expired) | Ubah dari String "1" ke Integer 1 |
Field Jumlah
| Field | Tipe | Deskripsi | Catatan Validasi |
|---|---|---|---|
sub_total | String | Jumlah bersih transaksi | Biarkan sebagai String |
total | String | Jumlah bruto transaksi | Biarkan sebagai String |
amount | String | Jumlah total transaksi | Biarkan sebagai String |
fee | String | Biaya admin | Biarkan sebagai String |
paid_off | Integer | Jumlah bersih diterima (amount - fee) | Ubah dari String ke Integer |
Field Timestamp
| Field | Tipe | Deskripsi | Catatan Validasi |
|---|---|---|---|
created_at | String | Waktu pembuatan transaksi | Format: YYYY-MM-DD HH:MM:SS |
expired_at | String | Waktu kedaluwarsa transaksi | - |
paid_at | String | Waktu pembayaran selesai | - |
settlement_status | String | Status settlement | settled atau unsettle |
Field Status
| Field | Tipe | Deskripsi | Catatan Validasi |
|---|---|---|---|
transaction_status_code | Integer | Kode status asli | Ubah dari String ke Integer |
is_escrow | Boolean | Indikator escrow | Ubah String "0" ke false, "1" ke true |
Detail Pembayaran
| Field | Tipe | Deskripsi | Catatan Validasi |
|---|---|---|---|
via | String | Metode pembayaran (contoh: va) | - |
channel | String | Channel pembayaran (contoh: bca, mandiri) | - |
payment_no | String | Nomor VA atau pembayaran | Penting: Jaga sebagai String agar angka 0 di depan tidak hilang |
va | String | Nomor VA Merchant | Sama seperti payment_no. Hanya ada untuk transaksi VA |
system_notes | String | Catatan yang dihasilkan sistem | - |
Informasi Pembeli
| Field | Tipe | Deskripsi | Catatan Validasi |
|---|---|---|---|
buyer_name | String | Nama pembeli | - |
buyer_email | String | Email pembeli | - |
buyer_phone | String | Nomor HP pembeli | - |
Field Tambahan
| Field | Tipe | Deskripsi | Catatan Validasi |
|---|---|---|---|
additional_info | Array | Informasi tambahan | Diperlukan: Sertakan [] bahkan jika field tidak dikirim |
url | String | URL Callback | - |
Field Bersyarat
Beberapa field memiliki perilaku yang berbeda tergantung pada metode pembayaran. Berikut yang perlu diperhatikan:
| Field | Perilaku |
|---|---|
transaction_status_code | 1 = Pembayaran langsung settle. 6 = Pembayaran diterima (settlement dapat diproses secara terpisah) |
payment_no | Terisi untuk transaksi VA. Kosong untuk pembayaran QRIS dan e-wallet |
va | Hanya ada untuk transaksi VA. Tidak disertakan untuk pembayaran QRIS dan e-wallet |
Validasi Signature
Untuk memastikan callback benar-benar berasal dari iPaymu, Anda harus memvalidasi signature pada setiap request. Berikut langkah-langkah prosesnya:
Langkah-langkah Validasi
- Ambil Data — Dapatkan semua data dari
req.body - Normalisasi Tipe — Ubah nilai ke tipe data yang benar
- String ke Integer:
trx_id,status_code,transaction_status_code,paid_off - String ke Boolean:
is_escrow - String
[]ke Array[]:additional_info
- String ke Integer:
- Tangani Field yang Hilang — Jika
additional_infotidak ada (umum terjadi denganx-www-form-urlencoded), tambahkan manual sebagai array kosong - Urutkan Key — Urutkan semua key secara Ascending (A-Z) dengan sensitivitas huruf besar/kecil
- Konversi ke JSON — Serialisasi object yang sudah diurutkan ke string JSON
- URL Escape (Opsional) — Escape garis miring (
/→\/) jika mengikuti standar ketatjson_encodePHP - Generate Hash — Buat HMAC-SHA256 menggunakan string JSON dan secret key (nomor VA) Anda
- Bandingkan — Cocokkan hash yang Anda buat dengan header
X-Signature
Contoh Kode Implementasi
import crypto from 'crypto';
import express from 'express';
const app = express();
app.use(express.urlencoded({ extended: true }));
function normalizeData(rawData) {
const result = {};
for (const key in rawData) {
let val = rawData[key];
if (key === 'is_escrow') {
result[key] = (val === 'true' || val === '1' || val === 1);
}
else if (['trx_id', 'status_code', 'transaction_status_code', 'paid_off'].includes(key)) {
result[key] = parseInt(val, 10);
}
else if (key === 'additional_info') {
if (val === '[]') result[key] = [];
else result[key] = val;
}
else {
result[key] = String(val);
}
}
if (!result.hasOwnProperty('additional_info')) {
result['additional_info'] = [];
}
return result;
}
function phpKsort(obj) {
return Object.keys(obj)
.sort((a, b) => a.localeCompare(b))
.reduce((sortedObj, key) => {
sortedObj[key] = obj[key];
return sortedObj;
}, {});
}
app.post('/callback', (req, res) => {
const secretKey = '123456'; // Nomor VA akun iPaymu Anda (lihat di Dashboard > Integration > API Key)
const receivedSignature = req.headers['x-signature'];
// Step 1 & 2: Ambil dan normalisasi data
const normalizedData = normalizeData(req.body);
if (normalizedData.signature) delete normalizedData.signature;
// Step 3 & 4: Sort key
const sortedData = phpKsort(normalizedData);
// Step 5 & 6: Konversi ke JSON dengan escape
let jsonBody = JSON.stringify(sortedData);
jsonBody = jsonBody.replace(/\//g, '\\/');
// Step 7: Generate hash
const calculatedSignature = crypto
.createHmac('sha256', secretKey)
.update(jsonBody)
.digest('hex');
// Step 8: Bandingkan signature
if (calculatedSignature === receivedSignature) {
console.log('✅ Signature valid');
// TODO: Update status transaksi di database
// TODO: Pastikan idempotency - cek apakah sudah diproses sebelumnya
res.status(200).json({ status: 'OK' });
} else {
console.log('❌ Signature tidak valid');
console.log('Expected:', receivedSignature);
console.log('Got: ', calculatedSignature);
res.status(400).send('Invalid Signature');
}
});
app.listen(PORT, () => {
console.log('═══════════════════════════════════════════════════');
console.log(' iPaymu Callback Handler - URL-encoded');
console.log('═══════════════════════════════════════════════════');
console.log(` Server running on http://localhost:${PORT}`);
console.log(` Endpoint: POST http://localhost:${PORT}/callback`);
console.log('═══════════════════════════════════════════════════');
});Panduan Debugging
Jika signature validation selalu gagal, ikuti langkah-langkah debugging berikut:
1. Log Semua Data
// Tambahkan logging detail di callback handler
app.post('/callback', (req, res) => {
console.log('=== CALLBACK DEBUG ===');
console.log('Headers:', JSON.stringify(req.headers, null, 2));
console.log('Body (raw):', req.body);
console.log('Content-Type:', req.headers['content-type']);
console.log('X-Signature:', req.headers['x-signature']);
// Lanjutkan validasi...
});2. Periksa JSON Output
// Print JSON yang digunakan untuk signature generation
const sortedData = phpKsort(normalizedData);
let jsonBody = JSON.stringify(sortedData);
jsonBody = jsonBody.replace(/\//g, '\\/');
console.log('JSON untuk signature:');
console.log(jsonBody);
// Output harus persis sama dengan yang diharapkan iPaymu3. Bandingkan Byte per Byte
// Jika signature tidak cocok, bandingkan karakter per karakter
function compareSignatures(sig1, sig2) {
console.log('Signature comparison:');
console.log('Expected:', sig1);
console.log('Got: ', sig2);
for (let i = 0; i < Math.max(sig1.length, sig2.length); i++) {
const c1 = sig1[i] || 'N/A';
const c2 = sig2[i] || 'N/A';
if (c1 !== c2) {
console.log(`Mismatch at index ${i}: '${c1}' vs '${c2}'`);
}
}
}Kesalahan Umum
Berikut adalah kesalahan yang sering terjadi saat implementasi callback:
1. Tidak Mengkonversi Tipe Data
Salah:
const data = req.body; // Langsung pakai tanpa normalisasiBenar:
const normalizedData = normalizeData(req.body);
// Pastikan trx_id, status_code, dll dalam tipe Integer2. Key Sorting Tidak Sesuai
Salah:
const sorted = Object.keys(data).sort(); // Mungkin case-insensitiveBenar:
const sorted = Object.keys(data).sort((a, b) => a.localeCompare(b));3. Tidak Escape Slash di JSON
Salah:
const jsonBody = JSON.stringify(sortedData); // URL tetap http://...Benar:
let jsonBody = JSON.stringify(sortedData);
jsonBody = jsonBody.replace(/\//g, '\\/'); // URL jadi http:\/\/...4. Menghapam additional_info
Salah:
// Tidak menambahkan additional_info jika tidak ada di requestBenar:
if (!result.hasOwnProperty('additional_info')) {
result['additional_info'] = [];
}5. Menggunakan Secret Key yang Salah
Salah:
const secretKey = 'api_key_atau_lainnya'; // Bukan VA NumberBenar:
const secretKey = '123456'; // Nomor VA akun iPaymu Anda (lihat di Dashboard > Integration > API Key)Checklist Implementasi
Sebelum go-live, pastikan Anda telah:
- Mengkonfigurasi callback URL di dashboard iPaymu
- Menggunakan VA Number sebagai secret key
- Mengimplementasikan normalisasi tipe data dengan benar
- Mengurutkan key secara ascending (A-Z)
- Meng-escape garis miring dalam JSON
- Merespons dengan HTTP 200 untuk request valid
- Mengimplementasikan idempotency (cek duplikat)
- Meng-log semua callback untuk debugging
- Menguji dengan data dummy dari iPaymu
- Memiliki fallback mechanism jika callback gagal
Sumber: Dokumentasi Callback iPaymu