Panduan · Terbit
Pindah dari TriPay ke Kasera Pay: pemetaan field transaction/create, callback X-Callback-Signature yang menjadi webhook Kasera-Signature-V1, dan respons {"success": false} yang tidak lagi memicu pengiriman ulang
Integrasi TriPay pindah ke Kasera Pay dengan dua file yang berubah: pemanggilan transaction/create dan file callback. Bentuk alurnya sama, yaitu buat tagihan, arahkan pembeli, terima notifikasi bertanda tangan HMAC-SHA256. Yang berubah ada enam: kredensial menyusut menjadi satu kunci, signature permintaan hilang, body menjadi JSON, kode channel berganti nama, tanda tangan callback ikut mengikat timestamp, dan keberhasilan callback dinilai dari kode HTTP, bukan dari JSON yang dikembalikan. Semua keterangan tentang TriPay di halaman ini dibaca dari tripay.co.id/developer pada 23 Oktober 2026, dan kode PHP di bawah diuji pada PHP 8.3 dan Postgres 16.
1. Kredensial dan alamat
TriPay memakai tiga hal: API key di header Authorization: Bearer, private key untuk membuat signature, dan kode merchant, masing-masing berbeda antara sandbox dan production, dengan URL sandbox sendiri di /api-sandbox/. Kasera Pay memakai satu kunci API di header yang sama. Kunci kp_test_ dan kp_live_ memanggil alamat yang sama, https://pay.kasera.id/v1/transactions, dan awalan kuncinya yang menentukan mode. Private key tidak punya padanan, dan baris hash_hmac('sha256', $merchantCode.$merchantRef.$amount, $privateKey) dihapus seluruhnya. Rahasia kedua yang dibutuhkan hanya signing secret webhook, yang dibuat terpisah untuk mode tes dan live.
2. Field transaction/create dan padanannya
| TriPay | Kasera Pay | Catatan |
|---|---|---|
| Body form (http_build_query) | Body JSON | Header Content-Type: application/json |
| method | payment_methods | Array. Satu kode langsung menerbitkan QR atau nomor VA; tanpa field ini pembeli memilih |
| merchant_ref | external_id | Label pesanan, bukan pencegah duplikat |
| amount | amount | Rupiah utuh |
| customer_name, customer_email, customer_phone | customer.name, customer.email, customer.phone | Telepon dalam format +628...; Virtual Account wajib customer.name |
| order_items | order_items | Hanya name, price, quantity; totalnya harus sama dengan amount |
| callback_url | Tidak ada per permintaan | Endpoint webhook diatur di dashboard |
| return_url | return_url | Pembeli kembali dengan ?id=payreq_...&status=succeeded |
| expired_time (unix, bawaan 24 jam) | expires_in_minutes (bawaan 60) | Bawaan yang jauh lebih pendek; setel eksplisit |
| signature | Tidak ada | Kunci API sudah cukup |
| Tidak ada | Header Idempotency-Key | Satu-satunya pencegah tagihan ganda |
Dua baris paling sering terlewat. Bawaan masa berlaku turun dari 24 jam menjadi 60 menit, jadi pesanan yang dulu boleh dibayar besok pagi kini kedaluwarsa dalam satu jam kalau expires_in_minutes tidak dikirim. Dan external_id yang sama pada dua permintaan tetap menghasilkan dua tagihan; hanya header Idempotency-Key yang membuat ketukan ganda mengembalikan tagihan yang sama. Kuncinya harus tetap untuk satu percobaan bayar, misalnya id pesanan ditambah nomor percobaan, seperti dijelaskan di dokumentasi idempotency.
Kode channel yang punya padanan: QRIS, QRISC, dan QRIS2 menjadi qris; BRIVA, BNIVA, MANDIRIVA, PERMATAVA, CIMBVA, DANAMONVA, dan MYBVA menjadi va_bri, va_bni, va_mandiri, va_permata, va_cimb, va_danamon, dan va_maybank. BCAVA, BSIVA, MUAMALATVA, OCBCVA, SAMPOERNAVA, OTHERBANKVA, dompet digital langsung seperti OVO, DANA, dan SHOPEEPAY, serta gerai seperti ALFAMART dan INDOMARET tidak punya padanan, dan mengirim kode yang tidak aktif ditolak dengan 422 payment_method_unavailable. Pembeli dompet digital tetap bisa membayar lewat QRIS. Selisih biaya per channel dibahas di perbandingan Kasera Pay dan TriPay.
<?php
// buat-tagihan.php: pengganti panggilan ke /api/transaction/create.
// kasera_pay.php diambil utuh dari panduan PHP Kasera Pay.
require __DIR__ . '/kasera_pay.php';
$tx = kasera_pay_create_transaction([
'amount' => $order['total'],
'description' => 'Pesanan ' . $order['id'],
'external_id' => $order['id'], // dulu merchant_ref
'payment_methods' => ['va_bri'], // dulu 'method' => 'BRIVA'
'customer' => [
'name' => $order['nama'], // dulu customer_name
'email' => $order['email'], // dulu customer_email
'phone' => $order['telepon'], // dulu customer_phone, kini +628...
],
'order_items' => $order['items'], // name, price, quantity
'return_url' => 'https://toko.example/pesanan/' . $order['id'],
'expires_in_minutes' => 60, // dulu expired_time (unix)
// Tidak ada lagi signature, callback_url, kode merchant, atau private key.
], 'order-' . $order['id'] . '-' . $order['attempt'], getenv('KASERA_PAY_KEY'));
// dulu $res->data->reference, pay_code, checkout_url
$simpan = [
'payment_request_id' => $tx['id'],
'nomor_va' => $tx['payment']['payment_code'] ?? null,
'checkout_url' => $tx['checkout_url'],
];Fungsi kasera_pay_create_transaction dan kasera_pay_verify_signature diambil utuh dari panduan PHP Kasera Pay, yang juga menjelaskan setiap barisnya.
3. Callback menjadi webhook
| Hal | TriPay | Kasera Pay |
|---|---|---|
| Header tanda tangan | X-Callback-Signature | Kasera-Signature-V1: t=<unix>,v1=<hex> |
| Yang ditandatangani | Body JSON mentah | Timestamp, titik, lalu body mentah |
| Kunci HMAC | Private key | Signing secret webhook |
| Tanda berhasil | Respons JSON {“success”: true} | Kode HTTP 2xx; isi body tidak dibaca |
| Pengiriman ulang | Jeda 2 menit, maksimal 3 kali | Hingga 7 kali dalam sekitar 33 jam |
| Waktu bayar | paid_at unix | paid_at ISO 8601 dengan zona waktu |
| Whitelist IP | IPv4 dan IPv6 diterbitkan | Tidak ada daftar IP; tanda tangan yang membuktikan pengirim |
Baris tanda berhasil itulah jebakan terbesar. Contoh callback PHP di dokumentasi TriPay menjawab kegagalan dengan exit(json_encode(['success' => false, ...])), yang dikirim dengan HTTP 200. Kalau pola itu ikut terbawa, misalnya saat database sedang mati, Kasera Pay membaca 200 sebagai terkirim dan tidak pernah mengulang, sehingga pembayaran yang sudah masuk tidak pernah tercatat. Kegagalan harus dijawab 500. Sebaliknya, karena satu event bisa datang berkali-kali selama 33 jam, id event wajib disimpan dengan indeks unik.
CREATE TABLE kasera_pay_events (
event_id text PRIMARY KEY,
created_at timestamptz NOT NULL DEFAULT now()
);
-- orders sudah ada dari integrasi TriPay; cukup tambah satu kolom.
ALTER TABLE orders ADD COLUMN payment_request_id text;<?php
// webhook-kasera.php: pengganti file callback TriPay.
require __DIR__ . '/kasera_pay.php';
function tangani_webhook_kasera(PDO $pdo, string $raw, ?string $header, string $secret): int
{
// Dulu: hash_hmac('sha256', $json, $privateKey) dibandingkan dengan
// X-Callback-Signature. Kini timestamp ikut ditandatangani.
if (! kasera_pay_verify_signature($raw, $header, $secret)) {
return 400;
}
$event = json_decode($raw, true);
$data = $event['data'] ?? [];
$pdo->beginTransaction();
try {
// Event yang sama bisa datang lebih dari sekali, dengan id yang sama.
$baru = $pdo->prepare('INSERT INTO kasera_pay_events (event_id) VALUES (?)
ON CONFLICT (event_id) DO NOTHING');
$baru->execute([$event['id']]);
if ($baru->rowCount() === 1) {
$q = $pdo->prepare('SELECT total, payment_request_id FROM orders
WHERE id = ? FOR UPDATE');
$q->execute([$data['external_id'] ?? '']);
$order = $q->fetch(PDO::FETCH_ASSOC);
if ($order && $event['type'] === 'payment.paid') { // dulu PAID
if ((int) $data['amount'] === (int) $order['total']) {
$pdo->prepare("UPDATE orders SET status = 'LUNAS' WHERE id = ?")
->execute([$data['external_id']]);
} else {
error_log('nominal tidak cocok: ' . $data['payment_request_id']);
}
} elseif ($order && in_array($event['type'], ['payment.expired', 'payment.failed'], true)) {
// dulu EXPIRED dan FAILED; hanya untuk tagihan yang terakhir dibuat
$pdo->prepare("UPDATE orders SET status = 'BATAL'
WHERE id = ? AND status <> 'LUNAS' AND payment_request_id = ?")
->execute([$data['external_id'], $data['payment_request_id']]);
}
}
$pdo->commit();
} catch (Throwable $e) {
$pdo->rollBack();
error_log('webhook kasera: ' . $e->getMessage());
return 500; // Kasera Pay mengirim ulang. Isi body tidak dibaca.
}
return 200;
}
if (PHP_SAPI !== 'cli') {
http_response_code(tangani_webhook_kasera(
new PDO(getenv('DATABASE_DSN')),
file_get_contents('php://input') ?: '',
$_SERVER['HTTP_KASERA_SIGNATURE_V1'] ?? null,
getenv('KASERA_PAY_WEBHOOK_SECRET') ?: '',
));
}Yang diuji pada kode ini: pembayaran sah dijawab 200 dan pesanan menjadi LUNAS; pengiriman ulang event yang sama dijawab 200 tanpa mengubah apa pun; secret yang salah, timestamp yang melenceng 400 detik, dan tanda tangan gaya TriPay atas body saja dijawab 400; dua entri v1 saat rotasi secret diterima; body yang di-decode lalu di-encode ulang gagal diverifikasi karena nama pelanggan berisi tanda & tiba sebagai \u0026; nominal yang tidak sama dengan total pesanan tidak melunasi; tabel orders yang hilang menghasilkan 500 tanpa menyimpan id event, lalu pengiriman ulang setelah tabel kembali melunasi pesanan. Tiga puluh pengiriman kembar bersamaan ke server PHP dengan 16 worker menghasilkan 30 jawaban 200, satu baris event, dan satu kali update. Setiap pengaman di atas juga dicopot satu per satu, dan uji yang menjaganya gagal.
4. Status yang tidak berpadanan satu-satu
- UNPAID menjadi
pending. - PAID menjadi
succeeded, dikabarkan lewat eventpayment.paid. - EXPIRED dan FAILED menjadi
expireddanfailed, lewatpayment.expireddanpayment.failed. Keduanya tidak final untuk uang: pembayaran yang tetap masuk sesudahnya mengubah status menjadisucceededdan mengirimpayment.paid. Kode di atas karenanya tidak pernah menolak pelunasan berdasarkan status BATAL. - REFUND tidak punya padanan. Pengembalian dana untuk QRIS dan Virtual Account dilakukan dengan transfer dari rekening penjual sendiri, dan status pembayarannya tetap
succeeded, seperti dijelaskan di dokumentasi refund. canceledhanya ada di Kasera Pay, dan membatalkan permintaan tidak membatalkan QR atau nomor Virtual Account yang sudah terbit.
5. Urutan mematikan TriPay
Jalankan keduanya berdampingan: pesanan baru ke Kasera Pay, file callback TriPay tetap hidup. Tagihan TriPay yang terbit sebelum peralihan masih bisa dibayar sampai expired_time-nya habis, yang bawaannya 24 jam dan bisa lebih panjang pada channel yang mengizinkan, jadi file callback lama baru boleh dimatikan setelah tagihan terakhir lewat masa berlakunya. Langkah peralihan bertahap, termasuk rekonsiliasi selama masa tumpang tindih, ada di panduan migrasi gateway tanpa downtime.
Pertanyaan yang sering muncul
Apakah ada SDK resmi, supaya tidak perlu menyalin fungsi PHP?
Ada. SDK PHP resmi dipasang dengan composer require kasera/kasera-pay dan butuh PHP 8.1 ke atas; ada juga SDK JavaScript lewat npm install kasera-pay. Untuk proyek PHP, SDK adalah jalur yang lebih pendek. Panduan ini memakai fungsi tanpa dependensi karena integrasi TriPay yang dipindahkan biasanya juga ditulis dengan curl biasa, sehingga perbedaannya terlihat baris demi baris.
Apa padanan open payment TriPay?
Halaman pembayaran permanen di pay.kasera.id/pp/nama-toko, tempat pembayar mengisi nominal, nama, dan catatan, dengan pilihan mengunci nominal, memasang daftar nominal, atau menetapkan minimum. Halaman itu diatur dari dashboard, bukan dibuat lewat API. Pembayaran yang masuk dari sana tetap mengirim webhook payment.paid yang sama.
Callback TriPay bisa diuji dengan Callback Tester. Bagaimana di Kasera Pay?
Pakai kunci kp_test_ di mode tes: permintaan pembayaran tes bisa diselesaikan lewat simulasi, lalu webhook payment.paid dikirim dengan tanda tangan yang sama seperti live, memakai signing secret mode tes. Endpoint live dan tes terpisah, masing-masing dengan secret-nya sendiri, jadi secret tes tidak akan pernah memverifikasi kiriman live.