Callback handler
Last updated:
The callback handler is a URL on your server that UnitPay calls when a payment changes state. It is the source of truth for whether a payment succeeded — never deliver goods or credit a balance based on the customer's browser redirect alone. Configure the URL in the merchant cabinet under Settings → Project → Payment handler URL.
Endpoint requirements
- HTTPS only. Plain HTTP is rejected by the cabinet validator.
- A registered domain name; raw IP addresses and embedded credentials are not accepted.
- The handler domain does not need to match your website domain.
https://api.example.com/unitpay/callbackis fine. - Respond within 10 seconds. Long-running work (fulfilment, emails) belongs in a background job, not in the response path.
Methods you must handle
| method | When it is sent | Your job |
|---|---|---|
check | Before the customer is asked for payment details. | Verify the order exists, the amount and currency match, and the customer is allowed to pay. Do not deliver yet. |
pay | After UnitPay confirms a successful charge. | Mark the order as paid in your system; deliver the goods or credit the balance. |
preauth | When funds are authorised but not captured yet. | Reserve inventory if relevant; do not deliver until pay arrives. |
error | An error happened on the gateway side. May be followed by a successful pay. | Log it; do not treat the order as failed unless no pay arrives within the payment window. |
check is disabled by default for new projects. Enable it from the cabinet only after your handler is verified to handle it correctly — rejecting check rejects the payment.
Request shape
UnitPay sends a GET request. Parameters arrive as query-string fields (or form-encoded body). Common fields:
| Parameter | Description |
|---|---|
method | One of check, pay, preauth, error. |
params[unitpayId] | UnitPay's unique payment identifier. Persist it on first sight; use it as the idempotency key. |
params[projectId] | Your project ID. Reject any callback for an unexpected project ID. |
params[account] | The customer or order identifier you passed to initPayment. |
params[orderSum] | Amount of the original order in major currency units. Compare against your stored order. |
params[orderCurrency] | ISO 4217 currency code of the original order. |
params[payerSum] | Amount actually charged to the customer (after FX, fees). |
params[payerCurrency] | Currency the customer was charged in. |
params[date] | Payment timestamp in YYYY-MM-DD HH:MM:SS WIB. |
params[test] | 1 for test-mode, 0 for live payments. |
params[signature] | Request signature. Verify before trusting any other parameter. |
Signature verification
Verify the signature first. Reject the request with HTTP 200 and an error message if it does not match — do not log the secret key.
- Take all keys under
paramsexceptsignandsignature. - Sort those keys alphabetically.
- Build a string by concatenating, in order:
method, then each sorted parameter value, then your project secret key. Use the literal four-character delimiter{up}between every part. - Hash the string with SHA-256 and lowercase the hex digest.
- Compare against
params[signature]using a constant-time comparison.
Reference implementation in Node.js:
import { createHash } from "node:crypto";
import { timingSafeEqual } from "node:crypto";
function verifyUnitpaySignature(method, params, secretKey) {
const sortedKeys = Object.keys(params)
.filter((k) => k !== "sign" && k !== "signature")
.sort();
const sortedValues = sortedKeys.map((k) => String(params[k]));
const payload = [method, ...sortedValues, secretKey].join("{up}");
const expected = createHash("sha256").update(payload).digest("hex");
const received = String(params.signature || "");
if (expected.length !== received.length) return false;
return timingSafeEqual(Buffer.from(expected), Buffer.from(received));
}
Expected response
Always reply with HTTP 200 and a JSON body. The shape tells UnitPay whether to advance the payment.
| Outcome | Body |
|---|---|
| Accept the request — payment may proceed (or has been processed). | {"result": {"message": "Request processed successfully."}} |
| Reject the request — payment must not proceed. | {"error": {"message": "Order not found."}} |
The error.message string is shown to the customer on the payment form, so write it as customer-facing copy. Do not include internal IDs, stack traces, or PII.
Idempotency
UnitPay may retry a callback if the network drops or your handler returns 5xx. Treat params[unitpayId] as the idempotency key:
- If the same
unitpayIdarrives twice with the samemethod, return the response you returned the first time. Do not deliver again, do not credit again. - If the customer retries a failed payment, UnitPay issues a new
unitpayId. The previousunitpayIdstays in your system as failed.
Funds are credited to your project balance based on UnitPay's record, not on the response your handler returned. If a delivery fails on your side after pay succeeded, retry from your background job — you do not need UnitPay to resend the callback.
What not to do
- Do not deliver during
preauth— funds are not yet captured. - Do not whitelist by IP only. UnitPay's outbound IPs may change; signature verification is the binding check.
- Do not log the secret key, the full callback URL with secrets in query strings, or the raw signature next to the verification result.
- Do not return HTTP 200 with an
errorbody for transient failures (database down, queue full). Return 5xx so UnitPay retries. - Do not skip
checkvalidation when it is enabled — an acceptedcheckcommits you to honour the eventualpay.
Operational notes
- If no handler URL is configured, callbacks are not sent and payments are auto-approved without your validation. Always set the URL before going live.
- Failed
paycallbacks (handler returned an error) leave the payment in Not completed status. Resolve the underlying issue, then retry from Statistics → Payment details in the cabinet.
Next steps
- Create a payment via API —
initPaymentreference. - Generate a payment link — cabinet and programmatic options.
- Refund a payment — reverse a charge after
paysucceeds.
Callback handler adalah URL pada server Anda yang dipanggil UnitPay ketika status pembayaran berubah. Ini adalah sumber kebenaran untuk menentukan apakah pembayaran berhasil — jangan pernah mengirimkan barang atau menambah saldo hanya berdasarkan redirect browser pelanggan. Atur URL pada kabinet merchant di Pengaturan → Proyek → URL callback handler.
Persyaratan endpoint
- Hanya HTTPS. HTTP biasa ditolak oleh validator kabinet.
- Nama domain terdaftar; alamat IP mentah dan kredensial tertanam tidak diterima.
- Domain handler tidak harus sama dengan domain situs web Anda.
https://api.example.com/unitpay/callbackdiperbolehkan. - Balas dalam 10 detik. Pekerjaan berdurasi panjang (fulfilment, email) sebaiknya dijalankan di background job, bukan pada jalur respons.
Metode yang harus Anda tangani
| method | Kapan dikirim | Tugas Anda |
|---|---|---|
check | Sebelum pelanggan diminta detail pembayaran. | Verifikasi pesanan ada, jumlah dan mata uang cocok, dan pelanggan diizinkan membayar. Jangan kirim barang dulu. |
pay | Setelah UnitPay mengonfirmasi penagihan berhasil. | Tandai pesanan sebagai lunas di sistem Anda; kirim barang atau tambahkan saldo. |
preauth | Saat dana diotorisasi tetapi belum dikapitalisasi. | Cadangkan stok jika relevan; jangan kirim barang sampai pay tiba. |
error | Terjadi error di sisi gateway. Bisa diikuti oleh pay yang sukses. | Catat dalam log; jangan tandai pesanan sebagai gagal kecuali pay tidak datang dalam jendela pembayaran. |
check dinonaktifkan secara default untuk proyek baru. Aktifkan dari kabinet hanya setelah handler Anda terverifikasi menanganinya dengan benar — menolak check berarti menolak pembayaran.
Bentuk permintaan
UnitPay mengirim permintaan GET. Parameter datang sebagai field query string (atau body form-encoded). Field umum:
| Parameter | Deskripsi |
|---|---|
method | Salah satu dari check, pay, preauth, error. |
params[unitpayId] | Identifikasi unik pembayaran di UnitPay. Simpan saat pertama kali muncul; gunakan sebagai kunci idempotensi. |
params[projectId] | ID proyek Anda. Tolak callback apa pun untuk ID proyek yang tidak diharapkan. |
params[account] | Identifikasi pelanggan atau pesanan yang Anda kirim ke initPayment. |
params[orderSum] | Jumlah pesanan asli dalam satuan mata uang utama. Bandingkan dengan pesanan yang Anda simpan. |
params[orderCurrency] | Kode mata uang ISO 4217 dari pesanan asli. |
params[payerSum] | Jumlah yang sebenarnya ditagihkan kepada pelanggan (setelah konversi mata uang dan biaya). |
params[payerCurrency] | Mata uang yang ditagihkan kepada pelanggan. |
params[date] | Stempel waktu pembayaran dalam format YYYY-MM-DD HH:MM:SS WIB. |
params[test] | 1 untuk mode uji, 0 untuk pembayaran live. |
params[signature] | Tanda tangan permintaan. Verifikasi sebelum mempercayai parameter lain. |
Verifikasi tanda tangan
Verifikasi tanda tangan terlebih dahulu. Tolak permintaan dengan HTTP 200 dan body error jika tidak cocok — jangan mencatat kunci rahasia.
- Ambil semua kunci di bawah
paramskecualisigndansignature. - Urutkan kunci tersebut secara alfabet.
- Bangun string dengan menggabungkan, secara berurutan:
method, lalu setiap nilai parameter terurut, lalu kunci rahasia proyek Anda. Gunakan pemisah literal empat karakter{up}di antara setiap bagian. - Hash string dengan SHA-256 dan ubah hex digest menjadi huruf kecil.
- Bandingkan dengan
params[signature]menggunakan perbandingan waktu-konstan.
Implementasi referensi dalam Node.js:
import { createHash } from "node:crypto";
import { timingSafeEqual } from "node:crypto";
function verifyUnitpaySignature(method, params, secretKey) {
const sortedKeys = Object.keys(params)
.filter((k) => k !== "sign" && k !== "signature")
.sort();
const sortedValues = sortedKeys.map((k) => String(params[k]));
const payload = [method, ...sortedValues, secretKey].join("{up}");
const expected = createHash("sha256").update(payload).digest("hex");
const received = String(params.signature || "");
if (expected.length !== received.length) return false;
return timingSafeEqual(Buffer.from(expected), Buffer.from(received));
}
Respons yang diharapkan
Selalu balas dengan HTTP 200 dan body JSON. Bentuknya memberi tahu UnitPay apakah pembayaran dapat dilanjutkan.
| Hasil | Body |
|---|---|
| Terima permintaan — pembayaran dapat dilanjutkan (atau telah diproses). | {"result": {"message": "Request processed successfully."}} |
| Tolak permintaan — pembayaran tidak boleh dilanjutkan. | {"error": {"message": "Order not found."}} |
String error.message ditampilkan kepada pelanggan pada formulir pembayaran, jadi tulis sebagai teks yang ramah pelanggan. Jangan menyertakan ID internal, jejak stack, atau PII.
Idempotensi
UnitPay dapat mengulang callback jika jaringan terputus atau handler Anda mengembalikan 5xx. Perlakukan params[unitpayId] sebagai kunci idempotensi:
- Jika
unitpayIdyang sama datang dua kali denganmethodyang sama, kembalikan respons yang Anda kirim pertama kali. Jangan mengirim ulang barang, jangan menambah saldo lagi. - Jika pelanggan mencoba ulang pembayaran yang gagal, UnitPay menerbitkan
unitpayIdbaru.unitpayIdsebelumnya tetap di sistem Anda sebagai gagal.
Dana dikreditkan ke saldo proyek Anda berdasarkan catatan UnitPay, bukan berdasarkan respons handler Anda. Jika pengiriman barang gagal di sisi Anda setelah pay berhasil, ulangi dari background job — Anda tidak perlu meminta UnitPay mengirim ulang callback.
Yang tidak boleh dilakukan
- Jangan kirim barang saat
preauth— dana belum dikapitalisasi. - Jangan whitelist hanya berdasarkan IP. IP keluar UnitPay dapat berubah; verifikasi tanda tangan adalah pemeriksaan yang mengikat.
- Jangan mencatat kunci rahasia, URL callback lengkap dengan rahasia di query string, atau tanda tangan mentah di sebelah hasil verifikasi.
- Jangan mengembalikan HTTP 200 dengan body
erroruntuk kegagalan sementara (database mati, antrian penuh). Kembalikan 5xx agar UnitPay mencoba ulang. - Jangan melewati validasi
checksaat diaktifkan — menerimacheckberarti Anda berjanji menghormatipayyang akan datang.
Catatan operasional
- Jika URL handler tidak dikonfigurasi, callback tidak dikirim dan pembayaran disetujui otomatis tanpa validasi Anda. Selalu atur URL sebelum go-live.
- Callback
payyang gagal (handler mengembalikan error) meninggalkan pembayaran dalam status Not completed. Selesaikan masalah penyebabnya, lalu coba ulang dari Statistik → Detail pembayaran di kabinet.
Langkah selanjutnya
- Membuat pembayaran melalui API — referensi
initPayment. - Membuat tautan pembayaran — opsi kabinet dan programatik.
- Pengembalian dana pembayaran — balik transaksi setelah
payberhasil.