Panduan · Terbit · Diperbarui

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

TriPayKasera PayCatatan
Body form (http_build_query)Body JSONHeader Content-Type: application/json
methodpayment_methodsArray. Satu kode langsung menerbitkan QR atau nomor VA; tanpa field ini pembeli memilih
merchant_refexternal_idLabel pesanan, bukan pencegah duplikat
amountamountRupiah utuh
customer_name, customer_email, customer_phonecustomer.name, customer.email, customer.phoneTelepon dalam format +628...; Virtual Account wajib customer.name
order_itemsorder_itemsHanya name, price, quantity; totalnya harus sama dengan amount
callback_urlTidak ada per permintaanEndpoint webhook diatur di dashboard
return_urlreturn_urlPembeli kembali dengan ?id=payreq_...&status=succeeded
expired_time (unix, bawaan 24 jam)expires_in_minutes (bawaan 60)Bawaan yang jauh lebih pendek; setel eksplisit
signatureTidak adaKunci API sudah cukup
Tidak adaHeader Idempotency-KeySatu-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.

Satu baris lagi berlaku mulai 26 Oktober 2026. Menurut Syarat Layanan Kasera Pay, pembayaran di atas Rp 1.000.000 sejak tanggal itu dikenai 1% atas seluruh nominal, baik lewat QRIS maupun Virtual Account, dan create untuk nominal itu wajib membawa delivery_date, delivery_terms_shown: true, description minimal 10 karakter, customer.name, serta customer.phone atau customer.email. Kalau salah satunya kurang, create ditolak 422 dengan kode galat yang menyebut field-nya, seperti tercantum di referensi create. Contoh di atas sudah mengirim kedua field pengiriman.

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)
    // Mulai 26 Oktober 2026 wajib untuk pembayaran di atas Rp 1.000.000:
    'delivery_date'        => $order['tanggal_kirim'], // YYYY-MM-DD, hari ini atau sesudahnya
    'delivery_terms_shown' => true,                  // checkout toko sudah menampilkan tanggal kirim
    // 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

HalTriPayKasera Pay
Header tanda tanganX-Callback-SignatureKasera-Signature-V1: t=<unix>,v1=<hex>
Yang ditandatanganiBody JSON mentahTimestamp, titik, lalu body mentah
Kunci HMACPrivate keySigning secret webhook
Tanda berhasilRespons JSON {“success”: true}Kode HTTP 2xx; isi body tidak dibaca
Pengiriman ulangJeda 2 menit, maksimal 3 kaliHingga 7 kali dalam sekitar 33 jam
Waktu bayarpaid_at unixpaid_at ISO 8601 dengan zona waktu
Whitelist IPIPv4 dan IPv6 diterbitkanTidak 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

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.

Mulai sekarang

Terima pembayaran pertama minggu ini.

Daftar gratis, buat payment link atau pakai API, dan lihat statusnya berubah otomatis. Tanpa biaya bulanan, biaya transaksi hanya untuk pembayaran yang berhasil.

Dapatkan di Google PlayDownload di App Store