API Merchant Trusted Pay
Terima pembayaran QRIS dan kirim penarikan ke rekening bank serta e-wallet lewat satu API JSON server-to-server.
Pengenalan
Semua berjalan lewat HTTPS dengan body JSON yang ditandatangani client secret Anda. Server Anda memanggil Trusted Pay; Trusted Pay memanggil balik server Anda saat pembayaran atau penarikan mencapai status akhir.
| Lingkungan | Base URL |
|---|---|
| Produksi | https://api.trustedpay.vip |
| Sandbox | https://staging.trustedpay.vip |
Base URL hanya membedakan sandbox dan produksi. Alamat ini permanen dan tidak pernah berubah, apa pun yang terjadi di balik API.
Yang Anda terima dari Trusted Pay
- Client ID: identitas akun Anda di setiap request.
- Client secret: untuk menandatangani request dan memverifikasi callback. Simpan hanya di server Anda.
- Login portal merchant: transaksi, saldo, settlement, dan log callback.
Yang Anda kirim ke Trusted Pay
- Alamat IP server yang akan memanggil API (IP whitelist).
- URL callback default (HTTPS, bisa diakses publik).
Jangan taruh di browser, aplikasi mobile, atau repository publik. Siapa pun yang punya secret dan IP yang di-whitelist bisa membuat penarikan. Kalau mungkin bocor, minta Trusted Pay menerbitkan yang baru; secret lama langsung tidak berlaku.
Mulai cepat
- Dapatkan credential. Terima Client ID dan client secret, lalu pastikan IP server Anda sudah di-whitelist.
- Buat deposit. Panggil
/api/order/createlalu tampilkanqrStringsebagai QR code untuk pelanggan. - Terima callback. Saat pelanggan membayar, Trusted Pay mengirim callback
PAIDyang ditandatangani kenotifyUrlAnda. Verifikasi signature-nya, lalu kreditkan pelanggan Anda. - Tarik dana. Cek
/api/withdrawal/balance, lalu panggil/api/withdrawal/create. Tunggu callbackSUCCESSatauFAILED. - Rekonsiliasi. Kalau callback terlambat atau request timeout, gunakan endpoint query dengan
orderIdyang sama.
Request & respons
- Semua endpoint memakai
POSTdenganContent-Type: application/json.signaturedikirim sebagai field di body JSON; tidak ada header otentikasi. - Nominal di request berupa bilangan bulat dalam IDR (tanpa desimal). Nominal di respons penarikan berupa string dengan dua desimal, mis.
"50000.00". - Waktu memakai format
YYYY-MM-DD HH:mm:ssdalam GMT+7. orderIdadalah referensi milik Anda dan harus unik per merchant untuk deposit, dan terpisah untuk penarikan. Gunakan huruf, angka,-dan_, maksimal 100 karakter.- Setiap respons ber-HTTP status 200 dengan field
code. Bacacode, bukan status HTTP.
{
"code": 200,
"message": "Success",
"data": { /* objek sesuai endpoint, null jika error */ }
}Anggap hasilnya belum diketahui. Jangan membuat transaksi baru dengan orderId berbeda. Panggil endpoint query dengan orderId yang sama, atau ulangi request create yang sama persis: request ini idempoten.
Signature
Gabungkan field di bawah dengan pemisah |, tambahkan client secret di akhir, hash dengan SHA-512, lalu kirim digest hex huruf kecil sebagai signature. Pakai nilai yang persis sama dengan yang ada di body; nominal ditulis sebagai bilangan bulat biasa.
| Endpoint | String signature |
|---|---|
/api/order/create | clientId|orderId|totalAmount|clientSecret |
/api/order/query | clientId|orderId|clientSecret |
/api/withdrawal/create | clientId|orderId|requestAmount|clientSecret |
/api/withdrawal/query | clientId|orderId|clientSecret |
/api/withdrawal/balance | clientId|method|clientSecret |
/api/withdrawal/balance-detail | clientId|clientSecret |
| Callback (dari Trusted Pay) | clientId|orderId|refCode|status|clientSecret |
Contoh perhitungan
| clientId | 8F3C2A9E1B7D4C6A0E5F9B2D7C1A3E8F |
| orderId / totalAmount | INV-1001 / 200000 |
| clientSecret | your-client-secret |
| String yang di-hash | 8F3C2A9E1B7D4C6A0E5F9B2D7C1A3E8F|INV-1001|200000|your-client-secret |
| signature | 0d80561a60fb1bdb3997158f84fb8d808ea2dc1cbe7357aeacb647840ea37abc0097547132d28a01d48a7d46658a4772e281a6608590ac264003b4d448e5b250 |
import hashlib def sign(*parts) -> str: """SHA-512 of the parts joined with '|' (client secret last), lowercase hex.""" return hashlib.sha512("|".join(str(p) for p in parts).encode("utf-8")).hexdigest() signature = sign(client_id, order_id, total_amount, client_secret)
function sign(...$parts): string { // SHA-512 of the parts joined with '|' (client secret last), lowercase hex. return hash('sha512', implode('|', $parts)); } $signature = sign($clientId, $orderId, $totalAmount, $clientSecret);
import { createHash } from "node:crypto"; // SHA-512 of the parts joined with '|' (client secret last), lowercase hex. const sign = (...parts) => createHash("sha512").update(parts.join("|"), "utf8").digest("hex"); const signature = sign(clientId, orderId, totalAmount, clientSecret);
import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.util.HexFormat; static String sign(String... parts) throws Exception { byte[] digest = MessageDigest.getInstance("SHA-512") .digest(String.join("|", parts).getBytes(StandardCharsets.UTF_8)); return HexFormat.of().formatHex(digest); // lowercase hex } String signature = sign(clientId, orderId, String.valueOf(totalAmount), clientSecret);
import ( "crypto/sha512" "encoding/hex" "strconv" "strings" ) func sign(parts ...string) string { sum := sha512.Sum512([]byte(strings.Join(parts, "|"))) return hex.EncodeToString(sum[:]) // lowercase hex } signature := sign(clientID, orderID, strconv.Itoa(totalAmount), clientSecret)
Error & pesan
code | Arti | Yang perlu dilakukan |
|---|---|---|
200 | Berhasil | Gunakan data. |
400 | Error validasi atau bisnis; message menjelaskan penyebabnya | Perbaiki request. Jangan diulang membabi buta. |
403 | IP address not allowed. | Panggil dari IP server yang di-whitelist, atau minta Trusted Pay menambahkan IP tersebut. |
429 | Terlalu banyak request | Perlambat dan ulangi setelah satu detik (lihat rate limit). |
Pesan yang sering muncul
| Pesan | Penyebab |
|---|---|
Invalid clientId. | Client ID tidak dikenal atau sudah dinonaktifkan. |
Invalid signature. | String signature, urutan field, format nominal, atau secret salah. |
Duplicate orderId. | orderId sudah dipakai dengan detail berbeda (atau untuk order yang sudah EXPIRED). |
Order is being processed. Please query the order status. | Deposit yang sama masih sedang dibuat. Cek statusnya beberapa detik lagi. |
Withdrawal is being processed. Please query the status. | Request penarikan yang sama sedang diproses. Cek statusnya. |
Payment gateway temporarily unavailable. Please try again. | QR belum bisa diterbitkan saat ini. Ulangi request yang sama. |
Payment channel is not configured. | Akun Anda belum diaktifkan untuk produk ini. Hubungi Trusted Pay. |
Minimum amount is … / Maximum amount is … | Nominal di luar limit akun Anda; pesan menyebutkan limitnya. |
Insufficient balance. | Saldo available lebih kecil dari nominal penarikan + fee. Tidak ada saldo yang dipotong. |
Account number could not be verified. | Rekening tujuan tidak ada atau tidak aktif. Tidak ada saldo yang dipotong. |
Bank is not available for this payout. / E-wallet is not available for this payout. | bankCode / ewalletCode tidak didukung (lihat kode). |
Withdrawal was rejected. | Penarikan ditolak. Tidak ada saldo yang dipotong. |
field: message | Ada field yang gagal validasi, mis. customerPhone: Phone number must start with 62. |
Pesan dari API selalu dalam bahasa Inggris, apa pun bahasa halaman ini.
Limit & rate limit
| Produk | Default per transaksi |
|---|---|
| Deposit (QRIS) | Rp 10.000 – Rp 10.000.000 |
| Penarikan | Rp 10.000 – Rp 50.000.000 (nominal penarikan, fee ditambahkan di atasnya) |
Akun Anda bisa memiliki limit berbeda. Pesan error selalu menyebutkan limit yang berlaku, dan /api/withdrawal/balance memberi nominal penarikan terbesar yang bisa Anda lakukan saat ini.
| Endpoint | Rate limit default |
|---|---|
/api/order/create | 10 request / detik |
/api/order/query | 5 request / detik |
/api/withdrawal/create | 5 request / detik |
/api/withdrawal/query, /balance, /balance-detail | 2 request / detik (bersama) |
Perubahan status dikirim ke notifyUrl Anda. Gunakan endpoint query untuk rekonsiliasi dan setelah timeout, bukan dalam loop rapat.
Deposit (QRIS)
POST/api/order/create
Membuat pembayaran QRIS dan mengembalikan isi QR untuk ditampilkan ke pelanggan.
| Field | Tipe | Keterangan |
|---|---|---|
clientId wajib | string | Client ID Anda. |
orderId wajib | string ≤ 100 | Referensi deposit unik milik Anda. |
totalAmount wajib | integer | Nominal (IDR) yang dibayar pelanggan. |
paymentType opsional | string | QRIS (default dan satu-satunya nilai). |
customerName wajib | string ≤ 255 | Nama pembayar. |
customerEmail wajib | string | Alamat email yang valid. |
customerPhone wajib | string | Diawali 62, hanya angka, 10–15 karakter, mis. 6281234567890. |
notifyUrl opsional | string ≤ 500 | URL callback untuk order ini. Default-nya URL callback akun Anda. Harus URL http(s) publik. |
returnUrl opsional | string | Halaman tujuan pelanggan setelahnya (disimpan, tidak dipakai Trusted Pay). |
signature wajib | string | SHA512(clientId|orderId|totalAmount|clientSecret) |
curl -X POST https://api.trustedpay.vip/api/order/create \ -H "Content-Type: application/json" \ -d '{ "clientId": "8F3C2A9E1B7D4C6A0E5F9B2D7C1A3E8F", "orderId": "INV-1001", "totalAmount": 200000, "paymentType": "QRIS", "customerName": "Andi", "customerEmail": "[email protected]", "customerPhone": "6281234567890", "notifyUrl": "https://merchant.example/callback/trustedpay", "signature": "0d80561a60fb1bdb3997158f84fb8d808ea2dc1cbe7357aeacb647840ea37abc0097547132d28a01d48a7d46658a4772e281a6608590ac264003b4d448e5b250" }'
import hashlib, requests BASE_URL = "https://api.trustedpay.vip" CLIENT_ID, CLIENT_SECRET = "8F3C2A9E1B7D4C6A0E5F9B2D7C1A3E8F", "your-client-secret" def sign(*parts): return hashlib.sha512("|".join(map(str, parts)).encode()).hexdigest() body = { "clientId": CLIENT_ID, "orderId": "INV-1001", "totalAmount": 200000, "paymentType": "QRIS", "customerName": "Andi", "customerEmail": "[email protected]", "customerPhone": "6281234567890", } body["signature"] = sign(CLIENT_ID, body["orderId"], body["totalAmount"], CLIENT_SECRET) res = requests.post(f"{BASE_URL}/api/order/create", json=body, timeout=40).json() if res["code"] == 200: qr_string = res["data"]["qrString"] # render as a QR code
$baseUrl = 'https://api.trustedpay.vip'; $clientId = '8F3C2A9E1B7D4C6A0E5F9B2D7C1A3E8F'; $clientSecret = 'your-client-secret'; $body = [ 'clientId' => $clientId, 'orderId' => 'INV-1001', 'totalAmount' => 200000, 'paymentType' => 'QRIS', 'customerName' => 'Andi', 'customerEmail' => '[email protected]', 'customerPhone' => '6281234567890', ]; $body['signature'] = hash('sha512', implode('|', [$clientId, $body['orderId'], $body['totalAmount'], $clientSecret])); $ch = curl_init("$baseUrl/api/order/create"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 40, CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode($body), ]); $res = json_decode(curl_exec($ch), true);
import { createHash } from "node:crypto"; const BASE_URL = "https://api.trustedpay.vip"; const CLIENT_ID = "8F3C2A9E1B7D4C6A0E5F9B2D7C1A3E8F", CLIENT_SECRET = "your-client-secret"; const sign = (...p) => createHash("sha512").update(p.join("|")).digest("hex"); const body = { clientId: CLIENT_ID, orderId: "INV-1001", totalAmount: 200000, paymentType: "QRIS", customerName: "Andi", customerEmail: "[email protected]", customerPhone: "6281234567890", }; body.signature = sign(CLIENT_ID, body.orderId, body.totalAmount, CLIENT_SECRET); const res = await (await fetch(`${BASE_URL}/api/order/create`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), })).json();
{
"code": 200,
"message": "Success",
"data": {
"refCode": "PX261021003514CR1RE6",
"transactionTime": "2026-10-21 00:35:14",
"orderId": "INV-1001",
"totalAmount": 200000,
"customerName": "Andi",
"customerEmail": "[email protected]",
"customerPhone": "6281234567890",
"notifyUrl": "https://merchant.example/callback/trustedpay",
"returnUrl": null,
"status": "PENDING",
"paidAt": null,
"expiredAt": "2026-10-21 01:05:14",
"qrString": "00020101021226610012ID.CO.QRIS.WWW…6304B896"
}
}- Tampilkan
qrStringsebagai QR code (library QR apa pun). Semua aplikasi bank atau e-wallet yang mendukung QRIS bisa membayarnya. refCodeadalah referensi Trusted Pay. Simpan berdampingan denganorderIdAnda.- QR berlaku 30 menit secara default;
expiredAtmemberi waktu pasti QR berhenti bisa dibayar.
POST/api/order/query
| Field | Tipe | Keterangan |
|---|---|---|
clientId wajib | string | Client ID Anda. |
orderId wajib | string | Referensi deposit Anda. |
signature wajib | string | SHA512(clientId|orderId|clientSecret) |
Mengembalikan objek data yang sama seperti create, dengan status dan paidAt terkini. Order yang tidak dikenal mengembalikan code: 400, Order not found.
Alur status order
| Status | Arti | Callback |
|---|---|---|
PENDING | QR sudah terbit, menunggu pelanggan membayar. | – |
PAID | Pembayaran terkonfirmasi. Nominal bersih dikreditkan ke saldo pending Anda. | Ya |
EXPIRED | Tidak dibayar tepat waktu. Status ini dipasang setelah jaringan pembayaran memastikan QR tidak dibayar, biasanya dalam 10 menit setelah expiredAt. | Tidak |
FAILED | QR tidak bisa diterbitkan. Anda boleh memanggil create lagi dengan orderId yang sama; refCode dan QR baru akan diterbitkan. | Tidak |
Kalau pelanggan membayar di detik terakhir dan jaringan melaporkannya terlambat, status order berubah dari EXPIRED menjadi PAID. Saldo Anda dikreditkan dan Anda menerima callback PAID seperti biasa. Selalu proses callback PAID yang valid, meskipun order itu sudah Anda anggap expired.
- Mengulang create dengan
orderIddantotalAmountyang sama mengembalikan order yang sudah ada (QR yang sama selama masihPENDING). orderIdyang sama dengantotalAmountberbeda, atau untuk order yangEXPIRED, mengembalikanDuplicate orderId.. GunakanorderIdbaru untuk pembayaran baru.
Penarikan (withdrawal)
POST/api/withdrawal/create
Mengirim dana dari saldo available Anda ke rekening bank atau e-wallet. Rekening tujuan diverifikasi sebelum dana bergerak.
| Field | Tipe | Keterangan |
|---|---|---|
clientId wajib | string | Client ID Anda. |
orderId wajib | string ≤ 100 | Referensi penarikan unik milik Anda. |
requestAmount wajib | integer | Nominal (IDR) yang diterima penerima. Fee penarikan ditambahkan di atasnya. |
method wajib | string | BANK_ACCOUNT atau EWALLET. |
bankCode | string | Kode bank 3 digit, wajib untuk BANK_ACCOUNT (lihat kode). |
ewalletCode | string | Wajib untuk EWALLET: DANA, GOPAY, OVO, SHOPEEPAY, LINKAJA, SAKUKU. |
accountNumber wajib | string ≤ 50 | Hanya angka. Untuk e-wallet, nomor HP yang terdaftar. |
accountName wajib | string ≤ 255 | Nama penerima menurut data Anda. Nama hasil verifikasi dikembalikan di respons. |
notifyUrl opsional | string ≤ 500 | URL callback untuk penarikan ini. Default-nya URL callback akun Anda. |
signature wajib | string | SHA512(clientId|orderId|requestAmount|clientSecret) |
curl -X POST https://api.trustedpay.vip/api/withdrawal/create \ -H "Content-Type: application/json" \ -d '{ "clientId": "8F3C2A9E1B7D4C6A0E5F9B2D7C1A3E8F", "orderId": "WD-1001", "requestAmount": 50000, "method": "BANK_ACCOUNT", "bankCode": "014", "accountNumber": "1234567890", "accountName": "Andi Wijaya", "signature": "<SHA512(clientId|WD-1001|50000|clientSecret)>" }'
body = {
"clientId": CLIENT_ID, "orderId": "WD-1001", "requestAmount": 50000,
"method": "EWALLET", "ewalletCode": "DANA",
"accountNumber": "081234567890", "accountName": "Andi Wijaya",
}
body["signature"] = sign(CLIENT_ID, body["orderId"], body["requestAmount"], CLIENT_SECRET)
res = requests.post(f"{BASE_URL}/api/withdrawal/create", json=body, timeout=90).json()
{
"code": 200,
"message": "Success",
"data": {
"refCode": "WX261021101502KD8Q2M",
"orderId": "WD-1001",
"accountNumber": "1234567890",
"accountName": "ANDI WIJAYA",
"bank": { "name": "BCA (Bank Central Asia)", "code": "014" },
"requestAmount": "50000.00",
"withdrawalFee": "1600.00",
"withdrawalAmount": "51600.00",
"status": "PENDING",
"statusRemark": null,
"requestAt": "2026-10-21 10:15:02",
"processedAt": null,
"payoutProvider": "TRUSTEDPAY",
"method": "BANK_ACCOUNT"
}
}withdrawalAmount=requestAmount+withdrawalFee, dipotong dari saldo available saat request diterima.accountNameadalah nama hasil verifikasi bank atau e-wallet dan bisa berbeda dari nama yang Anda kirim.payoutProviderselaluTRUSTEDPAY.- Siapkan timeout hingga 90 detik: rekening diverifikasi dulu sebelum transfer dikirim.
POST/api/withdrawal/query
Body: clientId, orderId, signature = SHA512(clientId|orderId|clientSecret). Mengembalikan objek data yang sama seperti create. Penarikan yang tidak dikenal mengembalikan Withdrawal not found.
POST/api/withdrawal/balance
Saldo Anda dan fee untuk metode penarikan tertentu. Body: clientId, method (BANK_ACCOUNT default, atau EWALLET), signature = SHA512(clientId|method|clientSecret).
{
"payoutProvider": "TRUSTEDPAY",
"method": "BANK_ACCOUNT",
"pendingBalance": "34580.00",
"availableBalance": "144000.00",
"withdrawalFee": "1600.00",
"maxRequestAmount": "142400.00",
"hasBalance": true
}maxRequestAmount adalah requestAmount terbesar yang bisa Anda kirim saat ini (saldo available dikurangi fee, dibatasi limit Anda). hasBalance bernilai true bila sudah mencapai minimum penarikan.
POST/api/withdrawal/balance-detail
Body: clientId, signature = SHA512(clientId|clientSecret). Mengembalikan { "pendingBalance": "34580.00", "availableBalance": "144000.00" }.
Alur status penarikan
| Status | Arti | Callback |
|---|---|---|
PENDING | Diterima dan saldo sudah dipotong; sedang dikirim. | – |
ON_PROCESS | Transfer sedang diproses di bank atau e-wallet. | – |
SUCCESS | Dana sudah sampai. processedAt terisi. | Ya |
FAILED | Dana tidak terkirim. withdrawalAmount dikembalikan otomatis ke saldo available Anda; statusRemark menjelaskan penyebabnya. | Ya |
- Kalau rekening tidak bisa diverifikasi, create mengembalikan
code: 400dan tidak ada saldo yang dipotong. - Mengulang create dengan
orderIdyang sama serta nominal, metode, bank atau e-wallet, dan rekening yang sama mengembalikan penarikan yang sudah ada: saldo tidak dipotong dua kali. orderIdyang sama dengan salah satu detail di atas berbeda mengembalikanDuplicate orderId..
Request yang timeout mungkin sebenarnya sudah diterima. Cek orderId aslinya (atau ulangi request yang identik) dan tunggu callback akhir. Membuat penarikan kedua dengan orderId baru bisa membuat penerima dibayar dua kali.
Saldo pending & available
- Deposit yang sudah dibayar dikreditkan, setelah dipotong fee, ke saldo pending Anda.
- Dana menjadi available setelah settlement. Settlement berjalan harian: deposit yang dibayar pada hari D (00:00:00–23:59:59 GMT+7) biasanya tersedia pada D+1 pukul 15:00 GMT+7, setiap hari termasuk akhir pekan.
- Ketersediaan dana mengikuti settlement yang sebenarnya, jadi sesekali bisa lebih lambat.
- Hanya saldo available yang bisa ditarik. Portal merchant menampilkan perkiraan waktu dana pending menjadi available.
Callback
Trusted Pay mengirim POST dengan body JSON ke notifyUrl Anda:
- Saat deposit menjadi
PAID. - Saat penarikan menjadi
SUCCESSatauFAILED.
| Aturan | Detail |
|---|---|
| Konfirmasi | Balas dengan HTTP 2xx apa pun dalam 5 detik. Kerjakan proses yang lama setelah membalas. |
| Pengulangan | Maksimal 6 kali percobaan. Jeda antar percobaan: 10 s, 30 s, 1 min, 5 min, 15 min. Redirect tidak diikuti. |
| Duplikat | Callback bisa datang lebih dari sekali. Proses secara idempoten berdasarkan refCode + status. |
| Urutan | Jangan bergantung pada urutan kedatangan; gunakan status. |
| Keamanan | Selalu verifikasi signature. Bisa juga dikonfirmasi lewat endpoint query. |
| Kirim ulang | Callback bisa dikirim ulang dari portal merchant (detail order atau penarikan). |
Callback deposit
{
"clientId": "8F3C2A9E1B7D4C6A0E5F9B2D7C1A3E8F",
"orderId": "INV-1001",
"refCode": "PX261021003514CR1RE6",
"totalAmount": 200000,
"status": "PAID",
"paidAt": "2026-10-21 00:37:07",
"rrn": "AX4UEDYO2VXN",
"signature": "SHA512(clientId|orderId|refCode|status|clientSecret)"
}rrn adalah referensi dari jaringan pembayaran (berguna untuk komplain pelanggan); bisa bernilai null.
Callback penarikan
{
"clientId": "8F3C2A9E1B7D4C6A0E5F9B2D7C1A3E8F",
"refCode": "WX261021101502KD8Q2M",
"orderId": "WD-1001",
"accountNumber": "1234567890",
"accountName": "ANDI WIJAYA",
"bank": { "name": "BCA (Bank Central Asia)", "code": "014" },
"method": "BANK_ACCOUNT",
"requestAmount": "50000.00",
"withdrawalFee": "1600.00",
"withdrawalAmount": "51600.00",
"status": "SUCCESS",
"statusRemark": null,
"requestAt": "2026-10-21 10:15:02",
"processedAt": "2026-10-21 10:15:31",
"signature": "SHA512(clientId|orderId|refCode|status|clientSecret)"
}statusRemark hanya terisi untuk FAILED.
Verifikasi callback
import hashlib, hmac from flask import Flask, request app = Flask(__name__) @app.post("/callback/trustedpay") def trustedpay_callback(): cb = request.get_json(force=True) expected = hashlib.sha512( "|".join([cb["clientId"], cb["orderId"], cb["refCode"], cb["status"], CLIENT_SECRET]).encode() ).hexdigest() if cb["clientId"] != CLIENT_ID or not hmac.compare_digest(expected, cb.get("signature", "").lower()): return {"ok": False}, 401 # idempotent: skip if (refCode, status) was already handled handle(cb) return {"ok": True}, 200
$cb = json_decode(file_get_contents('php://input'), true); $expected = hash('sha512', implode('|', [$cb['clientId'], $cb['orderId'], $cb['refCode'], $cb['status'], $clientSecret])); if ($cb['clientId'] !== $clientId || !hash_equals($expected, strtolower($cb['signature'] ?? ''))) { http_response_code(401); exit; } // idempotent: skip if (refCode, status) was already handled handle($cb); http_response_code(200); echo json_encode(['ok' => true]);
import express from "express"; import { createHash, timingSafeEqual } from "node:crypto"; const app = express(); app.post("/callback/trustedpay", express.json(), (req, res) => { const cb = req.body; const expected = createHash("sha512") .update([cb.clientId, cb.orderId, cb.refCode, cb.status, CLIENT_SECRET].join("|")).digest("hex"); const given = String(cb.signature || "").toLowerCase(); const ok = cb.clientId === CLIENT_ID && given.length === expected.length && timingSafeEqual(Buffer.from(given), Buffer.from(expected)); if (!ok) return res.status(401).json({ ok: false }); handle(cb); // idempotent by refCode + status res.json({ ok: true }); });
Kode bank & e-wallet
Kirim ewalletCode untuk penarikan EWALLET, atau bankCode 3 digit untuk penarikan BANK_ACCOUNT. Kode yang tidak ada di daftar ini ditolak sebelum dana bergerak.
E-wallet
| ewalletCode | E-wallet |
|---|---|
DANA | DANA |
GOPAY | GoPay |
OVO | OVO |
SHOPEEPAY | ShopeePay |
LINKAJA | LinkAja |
SAKUKU | Sakuku |
Bank (128)
| bankCode | Bank |
|---|---|
002 | BRI (Bank Rakyat Indonesia) |
008 | Bank Mandiri |
009 | BNI (Bank Negara Indonesia) |
011 | Bank Danamon |
013 | Bank Permata |
014 | BCA (Bank Central Asia) |
016 | Maybank Indonesia |
019 | Panin Bank |
022 | CIMB Niaga |
023 | UOB Indonesia (TMRW) |
028 | OCBC NISP |
031 | Citibank |
032 | JPMorgan Chase Bank |
033 | Bank of America |
036 | China Construction Bank Indonesia |
037 | Bank Artha Graha Internasional |
042 | MUFG Bank (Bank of Tokyo Mitsubishi UFJ) |
046 | DBS Indonesia |
047 | Bank Resona Perdania |
048 | Bank Mizuho Indonesia |
050 | Standard Chartered Bank |
054 | Bank Capital Indonesia |
057 | Bank BNP Paribas Indonesia |
061 | ANZ Indonesia |
067 | Deutsche Bank AG |
069 | Bank of China (Hong Kong) |
076 | Bank Bumi Arta |
087 | HSBC Indonesia |
088 | Bank Antardaerah |
089 | Rabobank International Indonesia |
095 | J Trust Bank |
097 | Bank Mayapada |
110 | Bank BJB |
111 | Bank DKI |
112 | BPD DIY |
113 | Bank Jateng |
114 | Bank Jatim |
115 | Bank Jambi |
116 | Bank Aceh Syariah |
117 | Bank Sumut |
118 | Bank Nagari (Sumbar) |
119 | Bank Riau Kepri |
120 | Bank Sumsel Babel |
121 | Bank Lampung |
122 | Bank Kalsel |
123 | Bank Kalbar |
124 | Bank Kaltimtara |
125 | Bank Kalteng |
126 | Bank Sulselbar |
127 | Bank SulutGo |
128 | Bank NTB Syariah |
129 | BPD Bali |
130 | Bank NTT |
131 | Bank Maluku Malut |
132 | Bank Papua |
133 | Bank Bengkulu |
134 | Bank Sulteng |
135 | Bank Sultra |
137 | Bank Banten |
145 | Bank Nusantara Parahyangan |
146 | Bank of India Indonesia |
147 | Bank Muamalat |
151 | Bank Mestika Dharma |
152 | Bank Shinhan Indonesia |
153 | Bank Sinarmas |
157 | Bank Maspion Indonesia |
161 | Bank Ganesha |
164 | ICBC Indonesia |
167 | QNB Indonesia |
200 | BTN (Bank Tabungan Negara) |
212 | Bank Woori Saudara |
213 | BTPN / Jenius |
253 | Bank Nano Syariah |
405 | Bank Victoria Syariah |
425 | BJB Syariah |
426 | Bank Mega |
441 | KB Bank (Bukopin / Wokee) |
459 | Krom Bank Indonesia |
472 | Bank Jasa Jakarta |
484 | KEB Hana / LINE Bank |
485 | MNC Bank (Motion) |
490 | Bank Neo Commerce |
494 | BRI Agroniaga |
498 | SBI Indonesia |
501 | blu / BCA Digital |
503 | Bank Nationalnobu |
506 | Bank Mega Syariah |
513 | Bank Ina Perdana |
517 | Panin Dubai Syariah |
520 | Bank Prima Master |
521 | KB Bukopin Syariah |
523 | Bank Sahabat Sampoerna |
526 | Bank Oke Indonesia |
531 | Bank Amar Indonesia |
535 | SeaBank |
536 | BCA Syariah |
542 | Bank Jago |
547 | BTPN Syariah |
548 | Bank Multi Arta Sentosa |
553 | Bank Mayora Indonesia |
555 | Bank Index Selindo |
559 | Bank CNB (Centratama Nasional) |
564 | Bank Mandiri Taspen |
566 | Bank Victoria International |
567 | Allo Bank |
600 | BPR LSB (ATMB LSB) |
688 | BPR KS |
699 | BPR Eka (Bank Eka) |
721 | Bank Permata UUS |
722 | Bank Danamon UUS |
723 | BTN UUS |
724 | Bank DKI UUS |
725 | Bank Jateng UUS |
727 | Bank Kalbar UUS |
728 | Bank Kalsel UUS |
729 | Bank Kaltimtara UUS |
733 | Bank Sulselbar UUS |
734 | Bank Sinarmas UUS |
736 | BPD DIY UUS |
777 | Finnet |
789 | Dompetku |
867 | BPR Eka |
899 | DOKU |
945 | Bank IBK Indonesia |
947 | Bank Aladin Syariah |
949 | CTBC Indonesia |
950 | Commonwealth Bank |
987 | ATMB Plus |
Checklist go-live
- IP server produksi sudah di-whitelist, dan client secret hanya disimpan di server.
- Signature dihitung dari nilai body yang persis sama, dalam hex SHA-512 huruf kecil.
- Endpoint callback memakai HTTPS dan bisa diakses publik, memverifikasi signature, membalas 2xx dalam 5 detik, dan idempoten berdasarkan
refCode+status. - Callback
PAIDtetap diproses meskipun order sudah Anda anggap expired. - Timeout dan 5xx diselesaikan lewat endpoint query. Penarikan tidak pernah dibuat ulang dengan
orderIdbaru. - Anda membaca field
codedi respons, bukan hanya status HTTP. - Alur penarikan Anda mengecek
/api/withdrawal/balancelebih dulu dan menanganiInsufficient balance.