Trusted Pay API Docs v1.0 https://api.trustedpay.vip

Trusted Pay Merchant API

Accept QRIS payments and send withdrawals to bank accounts and e-wallets with one server-to-server JSON API.

QRIS depositsBank & e-wallet withdrawalsSigned callbacksSHA-512 signaturesGMT+7 timestamps

Introduction

Everything runs over HTTPS with JSON bodies, signed with your client secret. Your server calls Trusted Pay; Trusted Pay calls your server back when a payment or withdrawal reaches its final status.

EnvironmentBase URL
Productionhttps://api.trustedpay.vip
Sandboxhttps://staging.trustedpay.vip

The base URL only tells sandbox and production apart. It is permanent: it never changes, whatever happens behind the API.

What you receive from Trusted Pay

  • Client ID: identifies your account in every request.
  • Client secret: signs requests and verifies callbacks. Keep it on your server only.
  • Merchant portal login: transactions, balances, settlements and callback logs.

What you send to Trusted Pay

  • Server IP addresses that will call the API (IP whitelist).
  • Default callback URL (HTTPS, publicly reachable).
Never expose the client secret

Do not put it in a browser, mobile app or public repository. Anyone with the secret and a whitelisted IP can create withdrawals. If it may have leaked, ask Trusted Pay to issue a new one; the old secret stops working immediately.

Quick start

  1. Get credentials. Receive your Client ID and client secret, and have your server IPs whitelisted.
  2. Create a deposit. Call /api/order/create and render qrString as a QR code for your customer.
  3. Receive the callback. When the customer pays, Trusted Pay posts a signed PAID callback to your notifyUrl. Verify the signature, then credit your customer.
  4. Withdraw. Check /api/withdrawal/balance, then call /api/withdrawal/create. Wait for the SUCCESS or FAILED callback.
  5. Reconcile. If a callback is late or a request timed out, use the query endpoints with the same orderId.

Requests & responses

  • Every endpoint is POST with Content-Type: application/json. The signature is a field in the JSON body; there are no auth headers.
  • Amounts in requests are whole numbers in IDR (no decimals). Amounts in withdrawal responses are strings with two decimals, e.g. "50000.00".
  • Times are YYYY-MM-DD HH:mm:ss in GMT+7.
  • orderId is your own reference and must be unique per merchant for deposits, and separately for withdrawals. Use letters, digits, - and _, up to 100 characters.
  • Every response has HTTP status 200 with a code field. Read code, not the HTTP status.
Response envelope
{
  "code": 200,
  "message": "Success",
  "data": { /* endpoint-specific object, null on errors */ }
}
HTTP 5xx or a network timeout

Treat the outcome as unknown. Do not create a new transaction with a different orderId. Call the query endpoint with the same orderId, or repeat the same create request: it is idempotent.

Signatures

Join the fields below with a pipe |, append your client secret, hash with SHA-512 and send the lowercase hex digest as signature. Use the exact values you put in the body; amounts are plain integers.

EndpointSignature string
/api/order/createclientId|orderId|totalAmount|clientSecret
/api/order/queryclientId|orderId|clientSecret
/api/withdrawal/createclientId|orderId|requestAmount|clientSecret
/api/withdrawal/queryclientId|orderId|clientSecret
/api/withdrawal/balanceclientId|method|clientSecret
/api/withdrawal/balance-detailclientId|clientSecret
Callbacks (from Trusted Pay)clientId|orderId|refCode|status|clientSecret

Worked example

clientId8F3C2A9E1B7D4C6A0E5F9B2D7C1A3E8F
orderId / totalAmountINV-1001 / 200000
clientSecretyour-client-secret
String to hash8F3C2A9E1B7D4C6A0E5F9B2D7C1A3E8F|INV-1001|200000|your-client-secret
signature0d80561a60fb1bdb3997158f84fb8d808ea2dc1cbe7357aeacb647840ea37abc0097547132d28a01d48a7d46658a4772e281a6608590ac264003b4d448e5b250
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)

Errors & messages

codeMeaningWhat to do
200SuccessUse data.
400Validation or business error; message says whyFix the request. Do not retry blindly.
403IP address not allowed.Call from a whitelisted server IP, or ask Trusted Pay to add the IP.
429Too many requestsSlow down and retry after a second (see rate limits).

Common messages

MessageCause
Invalid clientId.Unknown or deactivated Client ID.
Invalid signature.Wrong signature string, field order, amount format or secret.
Duplicate orderId.The orderId was already used with different details (or for an expired order).
Order is being processed. Please query the order status.The same deposit is still being created. Query it in a few seconds.
Withdrawal is being processed. Please query the status.The same withdrawal request is already in progress. Query it.
Payment gateway temporarily unavailable. Please try again.The QR could not be issued right now. Retry the same request.
Payment channel is not configured.Your account is not activated for this product yet. Contact Trusted Pay.
Minimum amount is … / Maximum amount is …Amount outside your account limits; the message states the limit.
Insufficient balance.Available balance is lower than request amount + fee. Nothing was debited.
Account number could not be verified.The beneficiary account does not exist or is inactive. Nothing was debited.
Bank is not available for this payout. / E-wallet is not available for this payout.Unsupported bankCode / ewalletCode (see codes).
Withdrawal was rejected.The withdrawal was declined. Nothing was debited.
field: messageA field failed validation, e.g. customerPhone: Phone number must start with 62.

API messages are always in English, whatever the language of this page.

Limits & rate limits

ProductDefault per transaction
Deposit (QRIS)Rp 10.000 – Rp 10.000.000
WithdrawalRp 10.000 – Rp 50.000.000 (request amount, fee added on top)

Your account can have different limits. The error message always states the limit that applied, and /api/withdrawal/balance returns the largest withdrawal you can make now.

EndpointDefault rate limit
/api/order/create10 requests / second
/api/order/query5 requests / second
/api/withdrawal/create5 requests / second
/api/withdrawal/query, /balance, /balance-detail2 requests / second (shared)
Prefer callbacks over polling

Status changes are pushed to your notifyUrl. Use the query endpoints for reconciliation and after timeouts, not in tight loops.

Deposit (QRIS)

POST/api/order/create

Creates a QRIS payment and returns the QR content to show your customer.

FieldTypeDescription
clientId requiredstringYour Client ID.
orderId requiredstring ≤ 100Your unique deposit reference.
totalAmount requiredintegerAmount in IDR the customer pays.
paymentType optionalstringQRIS (default and only value).
customerName requiredstring ≤ 255Payer name.
customerEmail requiredstringValid email address.
customerPhone requiredstringStarts with 62, digits only, 10–15 characters, e.g. 6281234567890.
notifyUrl optionalstring ≤ 500Callback URL for this order. Defaults to your account's callback URL. Must be a public http(s) URL.
returnUrl optionalstringWhere you send the customer afterwards (stored, not used by Trusted Pay).
signature requiredstringSHA512(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"
  }'
Response
{
  "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"
  }
}
  • Render qrString as a QR code (any QR library). Any QRIS-enabled banking or e-wallet app can pay it.
  • refCode is Trusted Pay's reference. Store it next to your orderId.
  • The QR is valid for 30 minutes by default; expiredAt gives the exact time it stops being payable.

POST/api/order/query

FieldTypeDescription
clientId requiredstringYour Client ID.
orderId requiredstringYour deposit reference.
signature requiredstringSHA512(clientId|orderId|clientSecret)

Returns the same data object as create, with the current status and paidAt. Unknown orders return code: 400, Order not found.

Order lifecycle

PENDING→PAID|EXPIRED|FAILED
StatusMeaningCallback
PENDINGQR issued, waiting for the customer.–
PAIDPayment confirmed. The net amount is credited to your pending balance.Yes
EXPIREDNot paid in time. Set once the payment network confirms the QR was not paid, usually within 10 minutes after expiredAt.No
FAILEDNo QR could be issued. You may call create again with the same orderId; a new refCode and QR are issued.No
A payment can arrive after EXPIRED

If the customer paid at the last moment and the network reports it late, the order changes from EXPIRED to PAID. Your balance is credited and you receive the normal PAID callback. Always honour a valid PAID callback, even for an order you already treated as expired.

Idempotency
  • Repeating create with the same orderId and totalAmount returns the existing order (same QR while it is PENDING).
  • The same orderId with a different totalAmount, or for an EXPIRED order, returns Duplicate orderId.. Use a new orderId for a new payment.

Withdrawal

POST/api/withdrawal/create

Sends money from your available balance to a bank account or e-wallet. The beneficiary account is verified before any money moves.

FieldTypeDescription
clientId requiredstringYour Client ID.
orderId requiredstring ≤ 100Your unique withdrawal reference.
requestAmount requiredintegerAmount in IDR the beneficiary receives. The withdrawal fee is added on top.
method requiredstringBANK_ACCOUNT or EWALLET.
bankCodestring3-digit bank code, required for BANK_ACCOUNT (see codes).
ewalletCodestringRequired for EWALLET: DANA, GOPAY, OVO, SHOPEEPAY, LINKAJA, SAKUKU.
accountNumber requiredstring ≤ 50Digits only. For e-wallets, the registered phone number.
accountName requiredstring ≤ 255Beneficiary name as you know it. The verified name is returned in the response.
notifyUrl optionalstring ≤ 500Callback URL for this withdrawal. Defaults to your account's callback URL.
signature requiredstringSHA512(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)>"
  }'
Response
{
  "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, debited from your available balance when the request is accepted.
  • accountName is the name verified with the bank or e-wallet and may differ from the name you sent.
  • payoutProvider is always TRUSTEDPAY.
  • Allow up to 90 seconds for the response: the account is verified before the transfer is submitted.

POST/api/withdrawal/query

Body: clientId, orderId, signature = SHA512(clientId|orderId|clientSecret). Returns the same data object as create. Unknown withdrawals return Withdrawal not found.

POST/api/withdrawal/balance

Your balance and the fee for a withdrawal method. Body: clientId, method (BANK_ACCOUNT default, or EWALLET), signature = SHA512(clientId|method|clientSecret).

Response data
{
  "payoutProvider": "TRUSTEDPAY",
  "method": "BANK_ACCOUNT",
  "pendingBalance": "34580.00",
  "availableBalance": "144000.00",
  "withdrawalFee": "1600.00",
  "maxRequestAmount": "142400.00",
  "hasBalance": true
}

maxRequestAmount is the largest requestAmount you can send now (available balance minus fee, capped by your limit). hasBalance is true when it reaches the minimum withdrawal.

POST/api/withdrawal/balance-detail

Body: clientId, signature = SHA512(clientId|clientSecret). Returns { "pendingBalance": "34580.00", "availableBalance": "144000.00" }.

Withdrawal lifecycle

PENDING→ON_PROCESS→SUCCESS|FAILED
StatusMeaningCallback
PENDINGAccepted and debited; being submitted.–
ON_PROCESSTransfer in progress at the bank or e-wallet.–
SUCCESSMoney delivered. processedAt is set.Yes
FAILEDNot delivered. withdrawalAmount is refunded automatically to your available balance; statusRemark says why.Yes
  • If the account cannot be verified, create returns code: 400 and nothing is debited.
  • Repeating create with the same orderId and the same amount, method, bank or e-wallet and account returns the existing withdrawal: nothing is debited twice.
  • The same orderId with any of those different returns Duplicate orderId..
Never resend a withdrawal under a new orderId after a timeout

A timed-out request may still have been accepted. Query the original orderId (or repeat the identical request) and wait for the final callback. Creating a second withdrawal with a new orderId can pay the beneficiary twice.

Pending & available balance

  • Paid deposits are credited, after fees, to your pending balance.
  • Funds become available once settled. Settlement runs daily: deposits paid on day D (00:00:00–23:59:59 GMT+7) are normally available on D+1 at 15:00 GMT+7, every day of the week.
  • Availability follows the actual settlement, so it can occasionally be later.
  • Only the available balance can be withdrawn. The merchant portal shows the expected release time of pending funds.

Callbacks

Trusted Pay sends a POST with a JSON body to your notifyUrl:

  • When a deposit becomes PAID.
  • When a withdrawal becomes SUCCESS or FAILED.
RuleDetail
AcknowledgeReturn any HTTP 2xx within 5 seconds. Do slow work after responding.
RetriesUp to 6 attempts. Waits between attempts: 10 s, 30 s, 1 min, 5 min, 15 min. Redirects are not followed.
DuplicatesA callback can arrive more than once. Process it idempotently by refCode + status.
OrderDo not rely on arrival order; use status.
SecurityAlways verify the signature. Optionally confirm with a query call.
ResendCallbacks can be resent from the merchant portal (order or withdrawal detail).

Deposit callback

{
  "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 is the payment network's reference (useful for customer disputes); it can be null.

Withdrawal callback

{
  "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 is filled only for FAILED.

Verify a 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

Bank & e-wallet codes

Send ewalletCode for EWALLET withdrawals, or the 3-digit bankCode for BANK_ACCOUNT withdrawals. Codes not listed here are rejected before any money moves.

E-wallets

ewalletCodeE-wallet
DANADANA
GOPAYGoPay
OVOOVO
SHOPEEPAYShopeePay
LINKAJALinkAja
SAKUKUSakuku

Banks (128)

bankCodeBank
002BRI (Bank Rakyat Indonesia)
008Bank Mandiri
009BNI (Bank Negara Indonesia)
011Bank Danamon
013Bank Permata
014BCA (Bank Central Asia)
016Maybank Indonesia
019Panin Bank
022CIMB Niaga
023UOB Indonesia (TMRW)
028OCBC NISP
031Citibank
032JPMorgan Chase Bank
033Bank of America
036China Construction Bank Indonesia
037Bank Artha Graha Internasional
042MUFG Bank (Bank of Tokyo Mitsubishi UFJ)
046DBS Indonesia
047Bank Resona Perdania
048Bank Mizuho Indonesia
050Standard Chartered Bank
054Bank Capital Indonesia
057Bank BNP Paribas Indonesia
061ANZ Indonesia
067Deutsche Bank AG
069Bank of China (Hong Kong)
076Bank Bumi Arta
087HSBC Indonesia
088Bank Antardaerah
089Rabobank International Indonesia
095J Trust Bank
097Bank Mayapada
110Bank BJB
111Bank DKI
112BPD DIY
113Bank Jateng
114Bank Jatim
115Bank Jambi
116Bank Aceh Syariah
117Bank Sumut
118Bank Nagari (Sumbar)
119Bank Riau Kepri
120Bank Sumsel Babel
121Bank Lampung
122Bank Kalsel
123Bank Kalbar
124Bank Kaltimtara
125Bank Kalteng
126Bank Sulselbar
127Bank SulutGo
128Bank NTB Syariah
129BPD Bali
130Bank NTT
131Bank Maluku Malut
132Bank Papua
133Bank Bengkulu
134Bank Sulteng
135Bank Sultra
137Bank Banten
145Bank Nusantara Parahyangan
146Bank of India Indonesia
147Bank Muamalat
151Bank Mestika Dharma
152Bank Shinhan Indonesia
153Bank Sinarmas
157Bank Maspion Indonesia
161Bank Ganesha
164ICBC Indonesia
167QNB Indonesia
200BTN (Bank Tabungan Negara)
212Bank Woori Saudara
213BTPN / Jenius
253Bank Nano Syariah
405Bank Victoria Syariah
425BJB Syariah
426Bank Mega
441KB Bank (Bukopin / Wokee)
459Krom Bank Indonesia
472Bank Jasa Jakarta
484KEB Hana / LINE Bank
485MNC Bank (Motion)
490Bank Neo Commerce
494BRI Agroniaga
498SBI Indonesia
501blu / BCA Digital
503Bank Nationalnobu
506Bank Mega Syariah
513Bank Ina Perdana
517Panin Dubai Syariah
520Bank Prima Master
521KB Bukopin Syariah
523Bank Sahabat Sampoerna
526Bank Oke Indonesia
531Bank Amar Indonesia
535SeaBank
536BCA Syariah
542Bank Jago
547BTPN Syariah
548Bank Multi Arta Sentosa
553Bank Mayora Indonesia
555Bank Index Selindo
559Bank CNB (Centratama Nasional)
564Bank Mandiri Taspen
566Bank Victoria International
567Allo Bank
600BPR LSB (ATMB LSB)
688BPR KS
699BPR Eka (Bank Eka)
721Bank Permata UUS
722Bank Danamon UUS
723BTN UUS
724Bank DKI UUS
725Bank Jateng UUS
727Bank Kalbar UUS
728Bank Kalsel UUS
729Bank Kaltimtara UUS
733Bank Sulselbar UUS
734Bank Sinarmas UUS
736BPD DIY UUS
777Finnet
789Dompetku
867BPR Eka
899DOKU
945Bank IBK Indonesia
947Bank Aladin Syariah
949CTBC Indonesia
950Commonwealth Bank
987ATMB Plus

Go-live checklist

  • Production server IPs are whitelisted, and the client secret is stored only on the server.
  • Signatures are computed from the exact body values, as lowercase SHA-512 hex.
  • The callback endpoint is HTTPS and public, verifies signatures, answers 2xx within 5 s, and is idempotent by refCode + status.
  • PAID callbacks are honoured even for orders you treated as expired.
  • Timeouts and 5xx are resolved with the query endpoints. Withdrawals are never re-created under a new orderId.
  • You read the response code field, not just the HTTP status.
  • Your withdrawal flow checks /api/withdrawal/balance first and handles Insufficient balance.