Panduan · Terbit
Integrasi payment gateway di n8n: HTTP Request yang aman diulang, webhook yang diverifikasi dari Raw Body, dan satu kueri Postgres sebagai penentu status lunas
Kasera Pay bisa dipasang di n8n dengan dua workflow dan tanpa satu baris Code node. Workflow pertama membuat permintaan pembayaran lewat HTTP Request dan mengirim checkout_url ke pembeli. Workflow kedua menerima webhook, memverifikasi tanda tangannya, dan menandai pesanan lunas di Postgres. Empat pengaturan menentukan hasilnya: opsi Raw Body di Webhook node, encoding UTF-8 yang disetel sendiri di Extract From File, jawaban lewat node Respond to Webhook setelah data tertulis, dan opsi Ignore Bots yang harus mati.
Semua perilaku node di halaman ini diperiksa terhadap dokumentasi dan kode sumber n8n saat panduan ini ditulis, dan kueri Postgres-nya dijalankan dengan 30 pengiriman event yang sama secara bersamaan. Kontraknya sama dengan stack lain: kalau backend-nya Node biasa, bentuk yang setara ada di panduan integrasi Node.js dan Express.
Alurnya, sebelum menyusun node
Pesanan masuk lewat pemicu apa pun: form, baris baru di spreadsheet, atau panggilan dari aplikasi. Workflow membaca total dari tabel orders, membuat permintaan pembayaran, menyimpan id-nya, lalu mengirim tautan pembayaran. Pembeli membayar dengan QRIS atau Virtual Account delapan bank. Kasera Pay mengirim payment.paid bertanda tangan ke URL webhook n8n, dan workflow kedua menandai pesanan lunas. Status permintaan pembayaran adalah pending, succeeded, dan expired; payment.paid adalah nama event-nya, bukan status.
CREATE TABLE orders (
id text PRIMARY KEY,
total integer NOT NULL CHECK (total > 0), -- rupiah utuh
status text NOT NULL DEFAULT 'BARU',
attempt integer NOT NULL DEFAULT 0,
payment_request_id text,
paid_at timestamptz
);
CREATE TABLE kasera_events (
id text PRIMARY KEY,
type text NOT NULL,
received_at timestamptz NOT NULL DEFAULT now()
);1. Workflow pembuat pembayaran
HTTP Request
Method POST
URL https://pay.kasera.id/v1/transactions
Authentication Generic Credential Type → Bearer Auth (kp_test_...)
Send Headers Idempotency-Key = order-{{ $json.id }}-{{ $json.attempt }}
Send Body JSON
{
"amount": {{ $json.total }},
"description": "Pesanan {{ $json.id }}",
"external_id": "{{ $json.id }}",
"return_url": "https://toko.example/pesanan/{{ $json.id }}"
}
Settings Retry On Fail: on
On Error: Continue (using error output)Kunci API disimpan sebagai kredensial Bearer Auth, bukan diketik di kolom header, supaya tidak ikut terbawa saat workflow diekspor atau dibagikan sebagai JSON. Nominalnya dibaca dari database lewat node Postgres sebelum HTTP Request, tidak pernah dari isian form. Form publik yang meneruskan angka ketikan pembeli ke amount membuat barang Rp 500.000 bisa dibayar Rp 1.000 dengan sah.
Retry On Fail aman dinyalakan justru karena header Idempotency-Key: kalau jawaban pertama hilang di jalan dan node mengulang, Kasera Pay mengembalikan permintaan yang sama dengan status 200, bukan membuat yang kedua. Tanpa header itu, setiap ulangan adalah tagihan baru; external_id hanya label dan tidak mencegah duplikat. Nomor percobaan ikut di kuncinya karena kunci yang sama selalu mengembalikan permintaan aslinya, termasuk yang sudah kedaluwarsa setelah 60 menit. Penjelasan lengkapnya ada di idempotency untuk pembayaran.
Jalur galat dari HTTP Request membawa jawaban seperti 422 amount_too_small untuk QRIS di bawah Rp 1.000 atau Virtual Account di bawah Rp 10.000. Sambungkan ke notifikasi untuk admin, bukan ke pembeli. Jalur berhasil disimpan dengan kueri ini, lalu checkout_url dikirim lewat node email atau WhatsApp:
UPDATE orders
SET payment_request_id = $1, status = 'MENUNGGU'
WHERE id = $2 AND status <> 'LUNAS';
-- Query Parameters: {{ $json.id }}, {{ $json.external_id }}2. Workflow penerima webhook
Webhook (POST, path kasera-pay-test)
Respond: Using 'Respond to Webhook' Node
Options: Raw Body = on
↓
Extract From File
Operation: Extract From Text File
Input Binary Field: data Destination Output Field: raw
Options: File Encoding = UTF-8, Keep Source = JSON
↓
Crypto
Action: Hmac Type: SHA256 Encoding: HEX
Credential: Hmac Secret = whsec_... (mode tes)
Property Name: hmac
Value: {{ ($('Webhook').item.json.headers['kasera-signature-v1'] ?? '').split(',')[0].slice(2) }}.{{ $json.raw }}
↓
If (AND)
{{ ($('Webhook').item.json.headers['kasera-signature-v1'] ?? '').split(',').slice(1).includes('v1=' + $json.hmac) }} is true
{{ Math.abs($now.toSeconds() - Number(($('Webhook').item.json.headers['kasera-signature-v1'] ?? '').split(',')[0].slice(2))) <= 300 }} is true
├─ false → Respond to Webhook (Text "bad signature", Response Code 400)
└─ true → Postgres (Execute Query) → Respond to Webhook (Text "ok", Response Code 200)Tanda tangan di header Kasera-Signature-V1 berbentuk t=<unix>,v1=<hex>, dengan v1 adalah HMAC-SHA256 atas t + “.” + body mentah. Body yang sudah di-parse n8n ke $json.body tidak bisa dipakai, karena menyusunnya ulang menjadi teks menghasilkan isi yang sama dengan byte yang berbeda. Opsi Raw Body membuat Webhook node menyimpan byte aslinya di binary data, dan Extract From File mengubahnya menjadi teks.
Dua opsi Extract From File tidak boleh dibiarkan kosong. Tanpa File Encoding, n8n menebak encoding dari isi berkas, dan tebakan yang meleset pada nama pembeli beraksen mengubah teksnya sehingga tanda tangan yang sah ditolak. Tanpa Keep Source, item keluaran tidak membawa data sebelumnya; karena itu header selalu dibaca lewat $('Webhook'). Header muncul dengan huruf kecil di headers. Signing secret dipakai utuh sebagai kunci HMAC, termasuk awalan whsec_. Crypto node versi baru menyimpannya sebagai kredensial Crypto di kolom Hmac Secret; versi lama memintanya langsung di node.
Syarat pertama di If node menerima tanda tangan dari secret mana pun yang sedang berlaku, karena selama rotasi signing secret header membawa dua entri v1. Syarat kedua menolak pengiriman yang selisih waktunya lebih dari lima menit, sehingga pengiriman lama yang direkam orang lain tidak bisa diputar ulang. Perbandingan string di If node tidak berjalan dalam waktu tetap seperti contoh di referensi webhook; batas lima menit itu membuat celahnya sangat sempit, tetapi bagi yang mengendalikan server n8n sendiri, Code node dengan modul crypto yang diizinkan adalah pilihan yang lebih ketat.
3. Satu kueri sebagai penentu status lunas
WITH baru AS (
INSERT INTO kasera_events (id, type) VALUES ($1, $2)
ON CONFLICT (id) DO NOTHING
RETURNING id
)
UPDATE orders o
SET status = CASE WHEN $2 = 'payment.paid' THEN 'LUNAS' ELSE 'KEDALUWARSA' END,
paid_at = CASE WHEN $2 = 'payment.paid' THEN now() ELSE o.paid_at END,
attempt = CASE WHEN $2 = 'payment.paid' THEN o.attempt ELSE o.attempt + 1 END
FROM baru
WHERE o.id = $3
AND ($2 = 'payment.paid'
OR (o.status <> 'LUNAS' AND o.payment_request_id = $4));
-- Query Parameters:
-- {{ $('Webhook').item.json.body.id }}, {{ $('Webhook').item.json.body.type }},
-- {{ $('Webhook').item.json.body.data.external_id }}, {{ $('Webhook').item.json.body.data.payment_request_id }}Pengiriman bersifat at-least-once, jadi event yang sama bisa tiba dua kali, kadang berbarengan. Pola “cari event dulu dengan satu node, lalu tulis dengan node lain” meninggalkan celah di antara keduanya. Kueri di atas menutupnya: INSERT ... ON CONFLICT DO NOTHING adalah penentunya, dan pesanan hanya diperbarui kalau event itu baru tercatat. Saat diuji dengan 30 eksekusi bersamaan untuk satu payment.expired, nomor percobaan naik tepat satu kali.
Node Remove Duplicates dengan operasi Remove Items Processed in Previous Executions kelihatan cocok, tetapi ia mencatat id saat node itu berjalan, sebelum Postgres menulis. Kalau penulisan sesudahnya gagal dan Kasera Pay mengirim ulang, pengiriman ulang itu dibuang sebagai duplikat dan pesanannya tidak pernah lunas.
Pemeriksaan payment_request_id menjaga urutan antar percobaan: kabar kedaluwarsa dari tagihan lama yang terlambat tiba tidak menimpa pesanan yang sedang menunggu tagihan baru. payment.paid selalu menang, termasuk yang menyusul setelah payment.expired. Event kedaluwarsa hanya dikirim ke endpoint yang mencentangnya di dashboard. Satu catatan tentang Query Parameters: nilainya dipisahkan koma, jadi jangan masukkan teks yang bisa berisi koma, seperti deskripsi atau nama.
4. URL, mode tes, dan batas waktu
Webhook node punya URL Test dan URL Production. URL Test hanya mendengarkan 120 detik setelah tombol Listen for test event ditekan, jadi yang didaftarkan di dashboard Kasera Pay, menu Developer, selalu URL Production. URL itu baru hidup setelah workflow dipublikasikan dan mati lagi saat publikasinya dicabut. Eksekusi lewat URL Production tidak tampil di kanvas; lihat di tab Executions.
Mode tes dan live adalah endpoint terpisah dengan signing secret masing-masing. Cara paling rapi adalah dua salinan workflow dengan path berbeda, misalnya kasera-pay-test dan kasera-pay-live, masing-masing dengan kredensial Crypto-nya sendiri. Dengan kunci kp_test_, halaman checkout menampilkan tombol simulasi, dan hasil berhasil maupun kedaluwarsa bisa dipicu lewat endpoint simulasi di dokumentasi mode tes.
Kasera Pay menunggu jawaban paling lama 10 detik per percobaan, dan URL-nya wajib https ke alamat publik, jadi n8n di laptop butuh terowongan. Jangan taruh langkah lambat, seperti mengirim WhatsApp ke pembeli atau memanggil model AI, sebelum Respond to Webhook. Pasang langkah itu di cabang sesudahnya, atau di workflow lain yang dipicu perubahan status. Sebelum memakai kunci live, cocokkan dengan checklist sebelum go-live.
Pertanyaan yang sering muncul
Kenapa pengiriman webhook ditolak 403 padahal workflow sudah dipublikasikan?
Periksa opsi Ignore Bots di Webhook node. Pengiriman Kasera Pay membawa User-Agent KaseraPay-Webhooks/1.0, dan pustaka pendeteksi bot yang dipakai n8n menggolongkannya sebagai bot. Dengan opsi itu menyala, n8n menjawab 403 sebelum workflow berjalan, jadi tab Executions kosong sementara dashboard Kasera Pay mencatat pengiriman yang gagal dan diulang. Matikan opsi itu untuk workflow webhook ini. Daftar izin IP juga tidak disarankan, karena alamat pengirim bukan bagian dari kontrak yang dipublikasikan; tanda tangan adalah pengamannya.
Bolehkah memakai Code node dan SDK resmi saja?
SDK JavaScript resmi memang jalur terpendek di proyek Node, tetapi Code node n8n secara bawaan tidak mengizinkan impor modul, baik modul bawaan seperti crypto maupun paket npm. Di n8n yang di-host sendiri, pemiliknya bisa membuka izin lewat variabel NODE_FUNCTION_ALLOW_BUILTIN dan NODE_FUNCTION_ALLOW_EXTERNAL, dan paketnya harus terpasang di node_modules milik n8n. Susunan node di panduan ini tidak bergantung pada izin itu, jadi berjalan sama di n8n Cloud maupun di server sendiri.
Apakah workflow boleh langsung menjawab 200 lalu mengerjakan sisanya?
Jangan untuk webhook pembayaran. Pilihan Respond: Immediately membuat n8n menjawab sebelum apa pun dicatat. Kalau Postgres sedang tidak bisa dihubungi, pesanan yang sudah dibayar tetap tercatat menunggu, dan Kasera Pay tidak akan mengirim ulang karena jawabannya sudah 2xx. Dengan node Respond to Webhook di akhir jalur, galat sebelum node itu membuat n8n menjawab 500, dan Kasera Pay mengulang pengiriman sampai tujuh kali dalam kurang lebih 33 jam.
Bisakah status pesanan disimpan di Google Sheets, bukan Postgres?
Bisa untuk volume kecil, dengan satu kelemahan yang perlu disadari: Sheets tidak bisa mencatat event dan memperbarui baris dalam satu langkah yang tidak terpisahkan, jadi dua pengiriman event yang sama yang tiba berbarengan bisa sama-sama lolos. Untuk payment.paid akibatnya hanya baris yang ditulis dua kali dengan isi sama. Untuk payment.expired, nomor percobaan bisa naik dua kali. Kalau Sheets yang dipakai, cara yang lebih ringkas ada di panduan integrasi Google Sheets dengan Apps Script.