Payment Infrastructure
for You
Integrate payin and payout capabilities with a single, unified API. RSA-secured, real-time webhooks.
Core Capabilities
Payin (Collection)
Accept payments via bank transfer, QRIS, and e-wallets. Hosted cashier page with configurable redirect.
Payout (Disbursement)
Send funds to 150+ Indonesian banks and e-wallets. Flexible fee handling with real-time status tracking.
RSA Encryption
Every request is signed with RSA-2048. Callback verification ensures data integrity end-to-end.
Authentication
All API requests must include two headers for authentication. The signature is generated using RSA-2048 with SHA-256 hashing.
| Header | Type | Description |
|---|---|---|
SIGN |
string | RSA signature of the request body (Base64 encoded) |
MCODE |
string | Your merchant code. Obtain it from Admin > Merchant Info > MCODE. |
Signature Generation
key=value pairs joined by &// Sorted parameter string:
[email protected]&merchantOid=7438bb79-...&name=test1¬ifyUrl=http://...&phone=08123456789&redirectUrl=https://...×tamp=12312311&type=payin&uid=28835218241114223x
// SHA-256 hash:
383fb0663e87ebae7c5e1debb68746eab0f73a16ec82be3d5ca388bec0254691
// RSA-signed (Base64):
a/WAwsxJQvYbfN18DHmK93RQbBQAQXLpETCBzrkK8a1cy+6M9BDX...=
Quick Start
Create your first payin order in minutes.
curl -X POST https://{domain}/api/v1/payin/create \
-H "Content-Type: application/json" \
-H "SIGN: <your_rsa_signature>" \
-H "MCODE: your_MCODE" \
-d '{
"uid": "user_2883",
"merchantOid": "9d6e7671-64b9-408b-abad-1ef4601af101",
"amount": 1000000,
"notifyUrl": "https://your-server.com/callback/payin",
"redirectUrl": "https://your-app.com/payment/success",
"timestamp": 1745723235,
"name": "Taylor",
"email": "[email protected]",
"phone": "08123456789",
"passage": "your_passage",
"type": "payin"
}'
import crypto from 'crypto';
const createPayin = async () => {
const body = {
uid: "user_2883",
merchantOid: crypto.randomUUID(),
amount: 1000000,
notifyUrl: "https://your-server.com/callback/payin",
redirectUrl: "https://your-app.com/payment/success",
timestamp: Math.floor(Date.now() / 1000),
name: "Taylor",
email: "[email protected]",
phone: "08123456789",
passage: "your_passage",
type: "payin"
};
const sign = generateRSASignature(body, privateKey);
const res = await fetch("https://{domain}/api/v1/payin/create", {
method: "POST",
headers: {
"Content-Type": "application/json",
"SIGN": sign,
"MCODE": "your_MCODE"
},
body: JSON.stringify(body)
});
return res.json(); // { code: 0, data: { oid, merchantOid, url } }
};
package main
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"net/http"
)
func createPayin() {
body := map[string]interface{}{
"uid": "user_2883",
"merchantOid": uuid.New().String(),
"amount": 1000000,
"notifyUrl": "https://your-server.com/callback/payin",
"redirectUrl": "https://your-app.com/payment/success",
"timestamp": time.Now().Unix(),
"name": "Taylor",
"email": "[email protected]",
"phone": "08123456789",
"type": "payin",
}
sorted := jsonToSortedString(body)
hash := sha256.Sum256([]byte(sorted))
hashHex := hex.EncodeToString(hash[:])
sign, _ := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256,
sha256.Sum256([]byte(hashHex))[:])
signB64 := base64.StdEncoding.EncodeToString(sign)
req, _ := http.NewRequest("POST",
"https://{domain}/api/v1/payin/create", body)
req.Header.Set("SIGN", signB64)
req.Header.Set("MCODE", "your_MCODE")
}
import hashlib, base64, json, time, uuid, requests
from Crypto.PublicKey import RSA
from Crypto.Signature import pkcs1_15
from Crypto.Hash import SHA256
def create_payin():
body = {
"uid": "user_2883",
"merchantOid": str(uuid.uuid4()),
"amount": 1000000,
"notifyUrl": "https://your-server.com/callback/payin",
"redirectUrl": "https://your-app.com/payment/success",
"timestamp": int(time.time()),
"name": "Taylor",
"email": "[email protected]",
"phone": "08123456789",
"type": "payin"
}
# Sort, hash, sign
sorted_str = "&".join(f"{k}={v}" for k, v in sorted(body.items()) if v)
hash_hex = hashlib.sha256(sorted_str.encode()).hexdigest()
key = RSA.import_key(open("private.pem").read())
signature = pkcs1_15.new(key).sign(SHA256.new(hash_hex.encode()))
sign_b64 = base64.b64encode(signature).decode()
resp = requests.post(
"https://{domain}/api/v1/payin/create",
json=body,
headers={
"SIGN": sign_b64,
"MCODE": "your_MCODE"
}
)
return resp.json()
import java.security.*;
import java.util.*;
public class PaymentAggregationSystem {
public static String createPayin() throws Exception {
Map<String, Object> body = new TreeMap<>();
body.put("uid", "user_2883");
body.put("merchantOid", UUID.randomUUID().toString());
body.put("amount", 1000000);
body.put("notifyUrl", "https://your-server.com/callback/payin");
body.put("redirectUrl", "https://your-app.com/payment/success");
body.put("timestamp", System.currentTimeMillis() / 1000);
body.put("name", "Taylor");
body.put("email", "[email protected]");
body.put("phone", "08123456789");
body.put("type", "payin");
// Sign: sort → SHA256 → RSA PKCS1v15 → Base64
String sorted = body.entrySet().stream()
.filter(e -> !e.getValue().toString().isEmpty())
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining("&"));
String hash = sha256Hex(sorted);
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initSign(privateKey);
sig.update(hash.getBytes());
String signB64 = Base64.getEncoder().encodeToString(sig.sign());
// POST with headers SIGN + MCODE
return httpPost("https://{domain}/api/v1/payin/create",
body, signB64, "your_MCODE");
}
}
$body = [
"uid" => "user_2883",
"merchantOid" => sprintf("%s-%s-%s-%s-%s", ...), // UUID
"amount" => 1000000,
"notifyUrl" => "https://your-server.com/callback/payin",
"redirectUrl" => "https://your-app.com/payment/success",
"timestamp" => time(),
"name" => "Taylor", "email" => "[email protected]",
"phone" => "08123456789", "type" => "payin"
];
$sign = PaymentAggregationSystem::sign($body, $privateKeyPem);
$ch = curl_init("https://{domain}/api/v1/payin/create");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"SIGN: " . $sign,
"MCODE: " . $mcode
],
CURLOPT_POSTFIELDS => json_encode($body),
CURLOPT_RETURNTRANSFER => true
]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);
{
"code": 0,
"data": {
"oid": "CLN270425tR53wb2W",
"merchantOid": "9d6e7671-64b9-408b-abad-1ef4601af101",
"url": "https://cashier.xxx.xxx/index.html?oid=CLN270425tR53wb2W"
},
"msg": "Operation successful"
}
API Reference
Base URL: https://{domain}/api/v1
/payin/create
Create a payin (collection) order. Returns a cashier URL for the user to complete payment.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
uid | string | Yes | User identifier |
merchantOid | string | Yes | Unique merchant order ID (UUID recommended) |
amount | integer | Yes | Amount in cents (e.g. 1000000 = IDR 10,000) |
notifyUrl | string | Yes | Webhook URL for async notifications |
redirectUrl | string | Yes | Redirect URL after payment |
timestamp | integer | Yes | Unix timestamp (seconds) |
name | string | Yes | Customer name |
email | string | Yes | Customer email |
phone | string | Yes | Customer phone |
passage | string | Yes | Payment channel code. Obtain available pay-in codes from Admin > Channel Info > Payment Code. |
type | string | Yes | Fixed: "payin" |
Response
| Field | Type | Description |
|---|---|---|
code | integer | 0 on success |
data.oid | string | System order ID |
data.merchantOid | string | Your merchant order ID (echo back) |
data.url | string | Cashier page URL — redirect the user here |
msg | string | "Operation successful" |
Code Examples
curl -X POST https://{domain}/api/v1/payin/create \
-H "Content-Type: application/json" \
-H "SIGN: <your_rsa_signature>" \
-H "MCODE: your_MCODE" \
-d '{
"uid": "user_2883",
"merchantOid": "9d6e7671-64b9-408b-abad-1ef4601af101",
"amount": 1000000,
"notifyUrl": "https://your-server.com/callback/payin",
"redirectUrl": "https://your-app.com/payment/success",
"timestamp": 1745723235,
"name": "Taylor",
"email": "[email protected]",
"phone": "08123456789",
"passage": "your_passage",
"type": "payin"
}'
const createPayin = async () => {
const body = {
uid: "user_2883",
merchantOid: crypto.randomUUID(),
amount: 1000000,
notifyUrl: "https://your-server.com/callback/payin",
redirectUrl: "https://your-app.com/payment/success",
timestamp: Math.floor(Date.now() / 1000),
name: "Taylor", email: "[email protected]",
phone: "08123456789", passage: "your_passage", type: "payin"
};
const sign = generateRSASignature(body, privateKey);
const res = await fetch("https://{domain}/api/v1/payin/create", {
method: "POST",
headers: { "Content-Type": "application/json", "SIGN": sign, "MCODE": MCODE },
body: JSON.stringify(body)
});
return res.json();
};
body := map[string]interface{}{
"uid": "user_2883", "merchantOid": uuid.New().String(),
"amount": 1000000, "notifyUrl": "https://your-server.com/callback/payin",
"redirectUrl": "https://your-app.com/payment/success",
"timestamp": time.Now().Unix(), "name": "Taylor",
"email": "[email protected]", "phone": "08123456789",
"type": "payin",
}
sorted := JsonToSortedString(body)
hash := sha256.Sum256([]byte(sorted))
hashHex := hex.EncodeToString(hash[:])
sig, _ := rsa.SignPKCS1v15(rand.Reader, privKey, crypto.SHA256,
sha256.Sum256([]byte(hashHex))[:])
signB64 := base64.StdEncoding.EncodeToString(sig)
req, _ := http.NewRequest("POST", "https://{domain}/api/v1/payin/create", jsonBody)
req.Header.Set("SIGN", signB64)
req.Header.Set("MCODE", mcode)
body = {
"uid": "user_2883", "merchantOid": str(uuid.uuid4()),
"amount": 1000000, "notifyUrl": "https://your-server.com/callback/payin",
"redirectUrl": "https://your-app.com/payment/success",
"timestamp": int(time.time()), "name": "Taylor",
"email": "[email protected]", "phone": "08123456789",
"type": "payin"
}
sorted_str = "&".join(f"{k}={v}" for k, v in sorted(body.items()) if v)
hash_hex = hashlib.sha256(sorted_str.encode()).hexdigest()
key = RSA.import_key(open("private.pem").read())
signature = pkcs1_15.new(key).sign(SHA256.new(hash_hex.encode()))
sign_b64 = base64.b64encode(signature).decode()
resp = requests.post("https://{domain}/api/v1/payin/create",
json=body, headers={"SIGN": sign_b64, "MCODE": MCODE})
Map<String, Object> body = new TreeMap<>();
body.put("uid", "user_2883");
body.put("merchantOid", UUID.randomUUID().toString());
body.put("amount", 1000000);
body.put("notifyUrl", "https://your-server.com/callback/payin");
body.put("redirectUrl", "https://your-app.com/payment/success");
body.put("timestamp", System.currentTimeMillis() / 1000);
body.put("name", "Taylor"); body.put("email", "[email protected]");
body.put("phone", "08123456789"); body.put("type", "payin");
String sorted = body.entrySet().stream()
.filter(e -> !e.getValue().toString().isEmpty())
.map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.joining("&"));
String hash = sha256Hex(sorted);
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initSign(privateKey); sig.update(hash.getBytes());
String signB64 = Base64.getEncoder().encodeToString(sig.sign());
// POST with headers SIGN + MCODE
$body = [
"uid" => "user_2883",
"merchantOid" => sprintf("%s-%s-%s-%s-%s", ...), // UUID
"amount" => 1000000,
"notifyUrl" => "https://your-server.com/callback/payin",
"redirectUrl" => "https://your-app.com/payment/success",
"timestamp" => time(),
"name" => "Taylor", "email" => "[email protected]",
"phone" => "08123456789", "type" => "payin"
];
$sign = PaymentAggregationSystem::sign($body, $privateKeyPem);
$ch = curl_init("https://{domain}/api/v1/payin/create");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"SIGN: " . $sign,
"MCODE: " . $mcode
],
CURLOPT_POSTFIELDS => json_encode($body),
CURLOPT_RETURNTRANSFER => true
]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);
{
"code": 0,
"data": {
"oid": "CLN270425tR53wb2W",
"merchantOid": "9d6e7671-64b9-408b-abad-1ef4601af101",
"url": "https://cashier.xxx.xxx/index.html?oid=CLN270425tR53wb2W"
},
"msg": "Operation successful"
}
/payout/create
Create a payout (disbursement) order to send funds to a bank account or e-wallet.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
uid | string | Yes | User identifier |
name | string | Yes | Beneficiary name |
merchantOid | string | Yes | Unique merchant order ID |
amount | integer | Yes | Amount in cents (e.g. 1000000 = IDR 10,000) |
bankCode | string | Yes | Bank code (see Bank Codes) |
accountNo | string | Yes | Beneficiary account number |
email | string | No | Beneficiary email |
notifyUrl | string | Yes | Webhook URL for status updates |
note | string | No | Remark / description |
feeType | string | Yes | "Additional" (fee deducted from merchant balance) or "Include" (fee deducted from the player order amount) |
passage | string | Yes | Payment channel code. Obtain available payout codes from Admin > Channel Info > Payment Code. |
type | string | Yes | Fixed: "payout" |
Code Examples
curl -X POST https://{domain}/api/v1/payout/create \
-H "Content-Type: application/json" \
-H "SIGN: <your_rsa_signature>" \
-H "MCODE: your_MCODE" \
-d '{
"uid": "3800",
"name": "John Doe",
"notifyUrl": "https://your-server.com/callback/payout",
"amount": 1500000,
"merchantOid": "d27eb86d-ada8-43cb-9d31-fa87dd578ab7",
"bankCode": "014",
"accountNo": "1234567890",
"email": "[email protected]",
"note": "payout",
"type": "payout",
"feeType": "Additional",
"passage": "your_passage"
}'
const createPayout = async () => {
const body = {
uid: "3800", name: "John Doe",
notifyUrl: "https://your-server.com/callback/payout",
amount: 1500000,
merchantOid: crypto.randomUUID(),
bankCode: "014", // BCA
accountNo: "1234567890",
email: "[email protected]",
note: "payout",
type: "payout",
feeType: "Additional",
passage: "your_passage"
};
const sign = generateRSASignature(body, privateKey);
const res = await fetch("https://{domain}/api/v1/payout/create", {
method: "POST",
headers: { "Content-Type": "application/json", "SIGN": sign, "MCODE": MCODE },
body: JSON.stringify(body)
});
return res.json();
};
body := map[string]interface{}{
"uid": "3800", "name": "John Doe",
"notifyUrl": "https://your-server.com/callback/payout",
"amount": 1500000,
"merchantOid": uuid.New().String(),
"bankCode": "014", // BCA
"accountNo": "1234567890",
"email": "[email protected]",
"note": "payout",
"type": "payout",
"feeType": "Additional",
"passage": "your_passage",
}
sorted := JsonToSortedString(body)
hash := sha256.Sum256([]byte(sorted))
hashHex := hex.EncodeToString(hash[:])
sig, _ := rsa.SignPKCS1v15(rand.Reader, privKey, crypto.SHA256,
sha256.Sum256([]byte(hashHex))[:])
signB64 := base64.StdEncoding.EncodeToString(sig)
req, _ := http.NewRequest("POST", "https://{domain}/api/v1/payout/create", jsonBody)
req.Header.Set("SIGN", signB64)
req.Header.Set("MCODE", mcode)
body = {
"uid": "3800", "name": "John Doe",
"notifyUrl": "https://your-server.com/callback/payout",
"amount": 1500000,
"merchantOid": str(uuid.uuid4()),
"bankCode": "014", # BCA
"accountNo": "1234567890",
"email": "[email protected]",
"note": "payout",
"type": "payout",
"feeType": "Additional",
"passage": "your_passage"
}
sorted_str = "&".join(f"{k}={v}" for k, v in sorted(body.items()) if v)
hash_hex = hashlib.sha256(sorted_str.encode()).hexdigest()
key = RSA.import_key(open("private.pem").read())
signature = pkcs1_15.new(key).sign(SHA256.new(hash_hex.encode()))
sign_b64 = base64.b64encode(signature).decode()
resp = requests.post("https://{domain}/api/v1/payout/create",
json=body, headers={"SIGN": sign_b64, "MCODE": MCODE})
Map<String, Object> body = new TreeMap<>();
body.put("uid", "3800"); body.put("name", "John Doe");
body.put("notifyUrl", "https://your-server.com/callback/payout");
body.put("amount", 1500000);
body.put("merchantOid", UUID.randomUUID().toString());
body.put("bankCode", "014"); // BCA
body.put("accountNo", "1234567890");
body.put("email", "[email protected]");
body.put("note", "payout");
body.put("type", "payout");
body.put("feeType", "Additional");
body.put("passage", "your_passage");
String sorted = body.entrySet().stream()
.filter(e -> !e.getValue().toString().isEmpty())
.map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.joining("&"));
String hash = sha256Hex(sorted);
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initSign(privateKey); sig.update(hash.getBytes());
String signB64 = Base64.getEncoder().encodeToString(sig.sign());
// POST with headers SIGN + MCODE
$body = [
"uid" => "3800", "name" => "John Doe",
"notifyUrl" => "https://your-server.com/callback/payout",
"amount" => 1500000,
"merchantOid" => uniqid("", true),
"bankCode" => "014", // BCA
"accountNo" => "1234567890",
"email" => "[email protected]",
"note" => "payout",
"type" => "payout",
"feeType" => "Additional",
"passage" => "your_passage"
];
$sign = PaymentAggregationSystem::sign($body, $privateKeyPem);
$ch = curl_init("https://{domain}/api/v1/payout/create");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"SIGN: " . $sign,
"MCODE: " . $mcode
],
CURLOPT_POSTFIELDS => json_encode($body),
CURLOPT_RETURNTRANSFER => true
]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);
{
"code": 0,
"data": {
"oid": "PAY200325mb7gKfOI",
"merchantOid": "d27eb86d-ada8-43cb-9d31-fa87dd578ab7"
},
"msg": "Operation successful"
}
/query/order
Query the status of a payin or payout order. Provide either oid or merchantOid.
| Field | Type | Description |
|---|---|---|
type | string | "payin" or "payout" |
oid | string | System order ID (either oid or merchantOid) |
merchantOid | string | Your merchant order ID (either oid or merchantOid) |
Code Examples
curl -X POST https://{domain}/api/v1/query/order \
-H "Content-Type: application/json" \
-H "SIGN: <your_rsa_signature>" \
-H "MCODE: your_MCODE" \
-d '{
"type": "payin",
"merchantOid": "9d6e7671-64b9-408b-abad-1ef4601af101"
}'
const queryOrder = async (type, merchantOid) => {
const body = { type, merchantOid };
const sign = generateRSASignature(body, privateKey);
const res = await fetch("https://{domain}/api/v1/query/order", {
method: "POST",
headers: { "Content-Type": "application/json", "SIGN": sign, "MCODE": MCODE },
body: JSON.stringify(body)
});
return res.json();
};
// Usage
const result = await queryOrder("payin", "9d6e7671-64b9-408b-abad-1ef4601af101");
body := map[string]interface{}{
"type": "payin",
"merchantOid": "9d6e7671-64b9-408b-abad-1ef4601af101",
}
sorted := JsonToSortedString(body)
hash := sha256.Sum256([]byte(sorted))
hashHex := hex.EncodeToString(hash[:])
sig, _ := rsa.SignPKCS1v15(rand.Reader, privKey, crypto.SHA256,
sha256.Sum256([]byte(hashHex))[:])
req, _ := http.NewRequest("POST", "https://{domain}/api/v1/query/order", jsonBody)
req.Header.Set("SIGN", base64.StdEncoding.EncodeToString(sig))
req.Header.Set("MCODE", mcode)
def query_order(order_type, merchant_oid):
body = {"type": order_type, "merchantOid": merchant_oid}
sorted_str = "&".join(f"{k}={v}" for k, v in sorted(body.items()) if v)
hash_hex = hashlib.sha256(sorted_str.encode()).hexdigest()
key = RSA.import_key(open("private.pem").read())
signature = pkcs1_15.new(key).sign(SHA256.new(hash_hex.encode()))
sign_b64 = base64.b64encode(signature).decode()
resp = requests.post("https://{domain}/api/v1/query/order",
json=body, headers={"SIGN": sign_b64, "MCODE": MCODE})
return resp.json()
# Usage
result = query_order("payin", "9d6e7671-64b9-408b-abad-1ef4601af101")
Map<String, Object> body = new TreeMap<>();
body.put("type", "payin");
body.put("merchantOid", "9d6e7671-64b9-408b-abad-1ef4601af101");
String sorted = body.entrySet().stream()
.map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.joining("&"));
String hash = sha256Hex(sorted);
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initSign(privateKey); sig.update(hash.getBytes());
String signB64 = Base64.getEncoder().encodeToString(sig.sign());
// POST to /query/order with headers SIGN + MCODE
$body = [
"type" => "payin",
"merchantOid" => "9d6e7671-64b9-408b-abad-1ef4601af101"
];
$sign = PaymentAggregationSystem::sign($body, $privateKeyPem);
$ch = curl_init("https://{domain}/api/v1/query/order");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"SIGN: " . $sign,
"MCODE: " . $mcode
],
CURLOPT_POSTFIELDS => json_encode($body),
CURLOPT_RETURNTRANSFER => true
]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);
// Payin order query response
{
"code": 0,
"data": {
"amount": 1000000,
"merchantOid": "9d6e7671-64b9-408b-abad-1ef4601af101",
"orderOid": "CLN270425tR53wb2W",
"timestamp": 1745723300,
"status": "order_success",
"settleStatus": "settle_success"
},
"msg": "Operation successful"
}
/query/balance
Query merchant account balance. For this endpoint, sign the MCODE value itself.
Response
| Field | Type | Description |
|---|---|---|
code | integer | 0 on success |
data.amount | integer | Available balance (cents) |
data.settleAmount | integer | Pending settlement balance (cents) |
data.freezeAmount | integer | Frozen balance (cents) |
msg | string | "Operation successful" |
Code Examples
# Note: SIGN is the RSA signature of your MCODE value itself
curl -X GET https://{domain}/api/v1/query/balance \
-H "SIGN: <rsa_sign_of_mcode>" \
-H "MCODE: your_MCODE"
const queryBalance = async () => {
// For balance query, sign the MCODE value itself
const hash = crypto.createHash("sha256").update(MCODE).digest("hex");
const sign = crypto.sign("sha256", Buffer.from(hash), privateKey);
const signB64 = sign.toString("base64");
const res = await fetch("https://{domain}/api/v1/query/balance", {
headers: { "SIGN": signB64, "MCODE": MCODE }
});
return res.json();
// { code: 0, data: { amount: 50000000, settleAmount: 10000000, freezeAmount: 0 }, msg: "Operation successful" }
};
// For balance query, sign the MCODE value itself
hash := sha256.Sum256([]byte(mcode))
hashHex := hex.EncodeToString(hash[:])
sig, _ := rsa.SignPKCS1v15(rand.Reader, privKey, crypto.SHA256,
sha256.Sum256([]byte(hashHex))[:])
signB64 := base64.StdEncoding.EncodeToString(sig)
req, _ := http.NewRequest("GET", "https://{domain}/api/v1/query/balance", nil)
req.Header.Set("SIGN", signB64)
req.Header.Set("MCODE", mcode)
def query_balance():
# For balance query, sign the MCODE value itself
hash_hex = hashlib.sha256(MCODE.encode()).hexdigest()
key = RSA.import_key(open("private.pem").read())
signature = pkcs1_15.new(key).sign(SHA256.new(hash_hex.encode()))
sign_b64 = base64.b64encode(signature).decode()
resp = requests.get("https://{domain}/api/v1/query/balance",
headers={"SIGN": sign_b64, "MCODE": MCODE})
return resp.json()
# {"code": 0, "data": {"amount": 50000000, "settleAmount": 10000000, "freezeAmount": 0}, "msg": "Operation successful"}
// For balance query, sign the MCODE value itself
String hash = sha256Hex(MCODE);
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initSign(privateKey);
sig.update(hash.getBytes());
String signB64 = Base64.getEncoder().encodeToString(sig.sign());
HttpURLConnection conn = (HttpURLConnection)
new URL("https://{domain}/api/v1/query/balance").openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("SIGN", signB64);
conn.setRequestProperty("MCODE", MCODE);
// For balance query, sign the MCODE value itself
$hashHex = hash("sha256", $mcode);
$privKey = openssl_pkey_get_private($privateKeyPem);
openssl_sign($hashHex, $signature, $privKey, OPENSSL_ALGO_SHA256);
$signB64 = base64_encode($signature);
$ch = curl_init("https://{domain}/api/v1/query/balance");
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => [
"SIGN: " . $signB64,
"MCODE: " . $mcode
],
CURLOPT_RETURNTRANSFER => true
]);
$result = json_decode(curl_exec($ch), true);
// {"code": 0, "data": {"amount": 50000000, "settleAmount": 10000000, "freezeAmount": 0}, "msg": "Operation successful"}
{
"code": 0,
"data": {
"amount": 50000000,
"settleAmount": 10000000,
"freezeAmount": 0
},
"msg": "Operation successful"
}
Webhooks
PaymentAggregationSystem sends asynchronous notifications to your notifyUrl when order status changes. You must respond with the string SUCCESS, otherwise PaymentAggregationSystem will retry up to 5 times.
ONEPAY-SIGN header — the server encrypts the SHA-256 body hash with your public key. Use your RSA private key to decrypt it, then compare the result against the SHA-256 hash of the sorted body parameters. Ensure idempotent handling — the same notification may arrive multiple times.
Payin Callback
Sent when a payin order is paid or settled. Non-realtime settlement orders may trigger two separate callbacks.
| Field | Type | Description |
|---|---|---|
amount | integer | Order amount (cents) |
merchantOid | string | Your merchant order ID |
orderOid | string | System order ID |
timestamp | integer | Unix timestamp |
status | string | Order status: order_success / order_fail / order_await / order_confirm / order_create / order_over |
settleStatus | string | Settlement status (see below) |
ONEPAY-MCODE (header) | string | Merchant code |
ONEPAY-SIGN (header) | string | RSA-encrypted hash — OnePay encrypts the body hash with your public key; decrypt with your private key to verify |
order_success | Payment received |
order_fail | Payment failed |
order_await | In progress |
order_confirm | Order created, payment method selected, but cashier page not yet opened |
order_create | Order created, payment method not selected, cashier page not yet opened |
order_over | Order timed out (sandbox mode only — production orders remain in order_await indefinitely) |
settle_success | Settled |
settle_await | Pending settlement |
settle_failed | Settlement failed |
settle_unknown | N/A (order not successful) |
In sandbox mode, the order result is automatically updated 30 seconds after order creation. Completed states trigger callbacks; pending payment does not trigger a callback, while direct creation failure does. Each order cycles through a different status result in sequence; place multiple orders to cover all scenarios. See the table below for details.
| No. | Order Status | Settlement Status | Description |
|---|---|---|---|
| 1 | order_over | settle_unknown | Order timed out |
| 2 | order_fail | settle_unknown | Payment failed |
| 3 | order_success | settle_failed | Payment succeeded, settlement failed |
| 4 | order_success | settle_await -> after 15s -> settle_success | Delayed settlement; a second callback will be sent |
| 5 | order_success | settle_await | Payment succeeded, settlement pending |
| 6 | order_success | settle_success | Fully successful |
| 7 | order_await | settle_unknown | Pending payment; no callback will be generated and the order remains waiting |
| 8 | order_fail | settle_unknown | Order creation failed directly; a callback will be sent |
POST <your-notifyUrl>
Content-Type: application/json
ONEPAY-MCODE: your_MCODE
ONEPAY-SIGN: <Base64-encoded RSA-encrypted SHA-256 hash of the sorted body params>
{
"amount": 500000,
"merchantOid": "59a69565-3936-4551-beab-f4b9b8ec899e",
"orderOid": "CLN310326tnPzGxBY",
"timestamp": 1774923798,
"status": "order_success",
"settleStatus": "settle_success"
}
# Verification steps:
# 1. Receive POST with HTTP headers ONEPAY-SIGN and ONEPAY-MCODE
# 2. Sort body params by key → key=value&key=value
# 3. SHA-256 hash the sorted string
# 4. Decrypt SIGN with your RSA private key (PKCS1v15)
# 5. Compare decrypted value with your hash
# 6. If match → process order → respond with "SUCCESS"
# 7. If mismatch → reject the callback
// Express.js handler
app.post("/callback/payin", async (req, res) => {
const sign = req.headers["onepay-sign"];
const body = req.body;
// Sort body params, hash, verify
const data = body;
const sorted = Object.keys(data).sort()
.filter(k => data[k] !== "")
.map(k => `${k}=${data[k]}`).join("&");
const hash = crypto.createHash("sha256").update(sorted).digest("hex");
// Decrypt signature with private key
const decrypted = crypto.privateDecrypt(
{ key: privateKey, padding: crypto.constants.RSA_PKCS1_PADDING },
Buffer.from(sign, "base64")
).toString();
if (decrypted !== hash) return res.status(400).send("FAIL");
// Process order (idempotent!)
if (data.status === "order_success") {
await updateOrderStatus(data.merchantOid, data.status, data.settleStatus);
}
res.send("SUCCESS");
});
func PayinCallback(c *gin.Context) {
sign := c.GetHeader("ONEPAY-SIGN")
var body map[string]interface{}
c.ShouldBindJSON(&body)
// Sort body params, hash
data := body
sorted := JsonToSortedString(data)
hash := sha256.Sum256([]byte(sorted))
hashHex := hex.EncodeToString(hash[:])
// Decrypt SIGN with private key
decrypted, err := DecryptWithPrivateKey(privKey, sign)
if err != nil || decrypted != hashHex {
c.String(400, "FAIL")
return
}
// Process order (idempotent)
status := data["status"].(string)
if status == "order_success" {
updateOrderStatus(data["merchantOid"].(string), status)
}
c.String(200, "SUCCESS")
}
# Flask handler
@app.route("/callback/payin", methods=["POST"])
def payin_callback():
sign = request.headers.get("ONEPAY-SIGN")
body = request.get_json()
# Sort body params, hash
data = body
sorted_str = "&".join(f"{k}={v}" for k, v in sorted(data.items()) if str(v))
hash_hex = hashlib.sha256(sorted_str.encode()).hexdigest()
# Decrypt signature with private key
key = RSA.import_key(open("private.pem").read())
cipher = PKCS1_v1_5.new(key)
decrypted = cipher.decrypt(base64.b64decode(sign), None).decode()
if decrypted != hash_hex:
return "FAIL", 400
# Process order (idempotent)
if data["status"] == "order_success":
update_order(data["merchantOid"], data["status"], data["settleStatus"])
return "SUCCESS"
// Spring Boot handler
@PostMapping("/callback/payin")
public String payinCallback(
@RequestHeader("ONEPAY-SIGN") String sign,
@RequestBody Map<String, Object> body) {
// Sort body params, hash
Map<String, Object> data = body;
String sorted = new TreeMap<>(data).entrySet().stream()
.filter(e -> !e.getValue().toString().isEmpty())
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining("&"));
String hash = sha256Hex(sorted);
// Decrypt signature with private key
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
String decrypted = new String(cipher.doFinal(Base64.getDecoder().decode(sign)));
if (!decrypted.equals(hash)) return "FAIL";
// Process order (idempotent)
if ("order_success".equals(data.get("status"))) {
updateOrder(data.get("merchantOid"), data.get("status"));
}
return "SUCCESS";
}
// Laravel/raw PHP callback handler
$sign = $_SERVER['HTTP_ONEPAY_SIGN'] ?? '';
$body = json_decode(file_get_contents('php://input'), true);
$data = $body;
// Sort body params, hash
ksort($data);
$pairs = [];
foreach ($data as $k => $v) {
if ((string)$v !== '') $pairs[] = "$k=$v";
}
$sorted = implode('&', $pairs);
$hashHex = hash('sha256', $sorted);
// Decrypt signature with private key
$privKey = openssl_pkey_get_private($privateKeyPem);
openssl_private_decrypt(base64_decode($sign), $decrypted, $privKey);
if ($decrypted !== $hashHex) {
http_response_code(400);
echo "FAIL"; exit;
}
// Process order (idempotent)
if ($data['status'] === 'order_success') {
updateOrder($data['merchantOid'], $data['status'], $data['settleStatus']);
}
echo "SUCCESS";
Payout Callback
Sent when a payout order status changes.
| Field | Type | Description |
|---|---|---|
amount | integer | Order amount (cents) |
merchantOid | string | Your merchant order ID |
orderOid | string | System order ID |
timestamp | integer | Unix timestamp |
status | string | order_success / order_fail / order_await / order_reverse |
ONEPAY-MCODE (header) | string | Merchant code |
ONEPAY-SIGN (header) | string | RSA-encrypted hash — OnePay encrypts the body hash with your public key; decrypt with your private key to verify |
order_success | Payout succeeded |
order_fail | Payout failed |
order_await | Payout in progress |
order_reverse | Reversed after success. The order succeeds first and fails later; after failure, the balance is refunded to the merchant. |
In sandbox mode, a status check is automatically triggered 30 seconds after order creation. Each order cycles through a different status result in sequence; place multiple orders to cover all scenarios. See the table below for details.
| No. | Order Status | Description |
|---|---|---|
| 1 | order_await | Pending; no callback will be generated |
| 2 | order_fail | Payout failed; a callback will be sent |
| 3 | order_success | Payout succeeded; a callback will be sent |
| 4 | order_success -> after 2min reverse -> order_reverse | Success first, then reversal after 2 minutes; two callbacks will be sent |
| 5 | order_fail | Order creation failed directly; a callback will be sent |
POST <your-notifyUrl>
Content-Type: application/json
ONEPAY-MCODE: your_MCODE
ONEPAY-SIGN: <Base64-encoded RSA-encrypted SHA-256 hash of the sorted body params>
{
"amount": 1500000,
"merchantOid": "6a6ab062-9a35-4a07-87cd-5ca0492c338c",
"orderOid": "PAY250325DoNhh8AY",
"timestamp": 1742899401,
"status": "order_success"
}
# Same verification flow as payin callback:
# 1. Sort body params by key → key=value&key=value
# 2. SHA-256 hash the sorted string
# 3. Decrypt SIGN with RSA private key
# 4. Compare → if match, process and respond "SUCCESS"
# Note: payout callback has no settleStatus field
app.post("/callback/payout", async (req, res) => {
const sign = req.headers["onepay-sign"];
const body = req.body;
const data = body;
const sorted = Object.keys(data).sort()
.filter(k => data[k] !== "")
.map(k => `${k}=${data[k]}`).join("&");
const hash = crypto.createHash("sha256").update(sorted).digest("hex");
const decrypted = crypto.privateDecrypt(
{ key: privateKey, padding: crypto.constants.RSA_PKCS1_PADDING },
Buffer.from(sign, "base64")
).toString();
if (decrypted !== hash) return res.status(400).send("FAIL");
if (data.status === "order_success") {
await completeWithdrawal(data.merchantOid, data.amount);
} else if (data.status === "order_fail") {
await refundWithdrawal(data.merchantOid);
} else if (data.status === "order_reverse") {
await reverseWithdrawal(data.merchantOid);
}
res.send("SUCCESS");
});
func PayoutCallback(c *gin.Context) {
sign := c.GetHeader("ONEPAY-SIGN")
var body map[string]interface{}
c.ShouldBindJSON(&body)
data := body
sorted := JsonToSortedString(data)
hash := sha256.Sum256([]byte(sorted))
hashHex := hex.EncodeToString(hash[:])
decrypted, err := DecryptWithPrivateKey(privKey, sign)
if err != nil || decrypted != hashHex {
c.String(400, "FAIL")
return
}
status := data["status"].(string)
switch status {
case "order_success":
completeWithdrawal(data["merchantOid"].(string))
case "order_fail":
refundWithdrawal(data["merchantOid"].(string))
case "order_reverse":
reverseWithdrawal(data["merchantOid"].(string))
}
c.String(200, "SUCCESS")
}
@app.route("/callback/payout", methods=["POST"])
def payout_callback():
sign = request.headers.get("ONEPAY-SIGN")
body = request.get_json()
data = body
sorted_str = "&".join(f"{k}={v}" for k, v in sorted(data.items()) if str(v))
hash_hex = hashlib.sha256(sorted_str.encode()).hexdigest()
key = RSA.import_key(open("private.pem").read())
cipher = PKCS1_v1_5.new(key)
decrypted = cipher.decrypt(base64.b64decode(sign), None).decode()
if decrypted != hash_hex:
return "FAIL", 400
if data["status"] == "order_success":
complete_withdrawal(data["merchantOid"])
elif data["status"] == "order_fail":
refund_withdrawal(data["merchantOid"])
elif data["status"] == "order_reverse":
reverse_withdrawal(data["merchantOid"])
return "SUCCESS"
@PostMapping("/callback/payout")
public String payoutCallback(
@RequestHeader("ONEPAY-SIGN") String sign,
@RequestBody Map<String, Object> body) {
Map<String, Object> data = body;
String sorted = new TreeMap<>(data).entrySet().stream()
.filter(e -> !e.getValue().toString().isEmpty())
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining("&"));
String hash = sha256Hex(sorted);
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
String decrypted = new String(cipher.doFinal(Base64.getDecoder().decode(sign)));
if (!decrypted.equals(hash)) return "FAIL";
switch (data.get("status").toString()) {
case "order_success": completeWithdrawal(data); break;
case "order_fail": refundWithdrawal(data); break;
case "order_reverse": reverseWithdrawal(data); break;
}
return "SUCCESS";
}
$sign = $_SERVER['HTTP_ONEPAY_SIGN'] ?? '';
$body = json_decode(file_get_contents('php://input'), true);
$data = $body;
// Sort body params, hash
ksort($data);
$pairs = [];
foreach ($data as $k => $v) {
if ((string)$v !== '') $pairs[] = "$k=$v";
}
$sorted = implode('&', $pairs);
$hashHex = hash('sha256', $sorted);
$privKey = openssl_pkey_get_private($privateKeyPem);
openssl_private_decrypt(base64_decode($sign), $decrypted, $privKey);
if ($decrypted !== $hashHex) {
http_response_code(400);
echo "FAIL"; exit;
}
switch ($data['status']) {
case 'order_success': completeWithdrawal($data); break;
case 'order_fail': refundWithdrawal($data); break;
case 'order_reverse': reverseWithdrawal($data); break;
}
echo "SUCCESS";
Signature Utilities
Complete signing and verification implementations. Copy these utility functions into your project — no SDK installation required.
package PaymentAggregationSystem
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"sort"
"strings"
)
// JsonToSortedString converts a JSON string to a sorted key=value&key=value string
func JsonToSortedString(jsonStr string) (string, error) {
var data map[string]interface{}
if err := json.Unmarshal([]byte(jsonStr), &data); err != nil {
return "", err
}
keys := make([]string, 0, len(data))
for k := range data {
keys = append(keys, k)
}
sort.Strings(keys)
pairs := make([]string, 0, len(keys))
for _, k := range keys {
var v string
switch val := data[k].(type) {
case string:
v = val
case float64:
v = fmt.Sprintf("%v", int64(val))
default:
v = fmt.Sprintf("%v", val)
}
if v != "" {
pairs = append(pairs, fmt.Sprintf("%s=%s", k, v))
}
}
return strings.Join(pairs, "&"), nil
}
// StringToSHA256 returns the hex-encoded SHA-256 hash
func StringToSHA256(s string) string {
hash := sha256.Sum256([]byte(s))
return hex.EncodeToString(hash[:])
}
// ImportPrivateKey imports an RSA private key from PEM (PKCS#1 or PKCS#8)
func ImportPrivateKey(privPEM string) (*rsa.PrivateKey, error) {
block, _ := pem.Decode([]byte(privPEM))
if block == nil {
return nil, errors.New("failed to parse private key PEM")
}
switch block.Type {
case "RSA PRIVATE KEY":
return x509.ParsePKCS1PrivateKey(block.Bytes)
case "PRIVATE KEY":
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil { return nil, err }
rsaKey, ok := key.(*rsa.PrivateKey)
if !ok { return nil, errors.New("not an RSA private key") }
return rsaKey, nil
default:
return nil, errors.New("unknown private key format")
}
}
// SignWithPrivateKey signs a message with RSA PKCS1v15 and returns Base64
func SignWithPrivateKey(privateKey *rsa.PrivateKey, message string) (string, error) {
hashed := sha256.Sum256([]byte(message))
signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hashed[:])
if err != nil { return "", err }
return base64.StdEncoding.EncodeToString(signature), nil
}
// DecryptWithPrivateKey decrypts Base64-encoded ciphertext with RSA PKCS1v15
func DecryptWithPrivateKey(privateKey *rsa.PrivateKey, ciphertextBase64 string) (string, error) {
ciphertext, err := base64.StdEncoding.DecodeString(ciphertextBase64)
if err != nil { return "", err }
plaintext, err := rsa.DecryptPKCS1v15(rand.Reader, privateKey, ciphertext)
if err != nil { return "", err }
return string(plaintext), nil
}
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.*;
import java.util.stream.Collectors;
import javax.crypto.Cipher;
public class PaymentAggregationSystem {
/** Sort JSON params → key=value&key=value */
public static String jsonToSortedString(Map<String, Object> data) {
return new TreeMap<>(data).entrySet().stream()
.filter(e -> !e.getValue().toString().isEmpty())
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining("&"));
}
/** SHA-256 hex digest */
public static String stringToSHA256(String input) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(input.getBytes());
StringBuilder hex = new StringBuilder();
for (byte b : hash) {
String h = Integer.toHexString(0xff & b);
if (h.length() == 1) hex.append('0');
hex.append(h);
}
return hex.toString();
}
/** Import RSA private key from PKCS#8 PEM */
public static PrivateKey importPrivateKey(String privPEM) throws Exception {
String cleaned = privPEM
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s", "");
byte[] encoded = Base64.getDecoder().decode(cleaned);
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(encoded);
return KeyFactory.getInstance("RSA").generatePrivate(spec);
}
/** RSA PKCS1v15 sign → Base64 */
public static String signWithPrivateKey(PrivateKey key, String message) throws Exception {
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initSign(key);
sig.update(message.getBytes());
return Base64.getEncoder().encodeToString(sig.sign());
}
/** RSA PKCS1v15 decrypt → plaintext */
public static String decryptWithPrivateKey(PrivateKey key, String ciphertextB64) throws Exception {
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(ciphertextB64));
return new String(decrypted);
}
/** Complete: sort → SHA256 → RSA sign → Base64 */
public static String generateSign(Map<String, Object> body, String privPEM) throws Exception {
String sorted = jsonToSortedString(body);
String hash = stringToSHA256(sorted);
PrivateKey pk = importPrivateKey(privPEM);
return signWithPrivateKey(pk, hash);
}
/** Verify callback: sort → SHA256 → decrypt sign → compare */
public static boolean verifyCallback(Map<String, Object> body, String signHeader, String privPEM) throws Exception {
String sorted = jsonToSortedString(body);
String hash = stringToSHA256(sorted);
PrivateKey pk = importPrivateKey(privPEM);
String decrypted = decryptWithPrivateKey(pk, signHeader);
return hash.equals(decrypted);
}
}
<?php
class PaymentAggregationSystem {
/** Sort params → key=value&key=value */
public static function jsonToSortedString($data) {
ksort($data);
$pairs = [];
foreach ($data as $k => $v) {
if ((string)$v !== '') $pairs[] = "$k=$v";
}
return implode('&', $pairs);
}
/** SHA-256 hex digest */
public static function stringToSHA256($str) {
return hash('sha256', $str);
}
/** RSA PKCS1v15 sign → Base64 */
public static function signWithPrivateKey($privateKeyPem, $message) {
$privKey = openssl_pkey_get_private($privateKeyPem);
openssl_sign($message, $signature, $privKey, OPENSSL_ALGO_SHA256);
return base64_encode($signature);
}
/** RSA PKCS1v15 decrypt → plaintext */
public static function decryptWithPrivateKey($privateKeyPem, $ciphertextB64) {
$privKey = openssl_pkey_get_private($privateKeyPem);
openssl_private_decrypt(base64_decode($ciphertextB64), $plaintext, $privKey);
return $plaintext;
}
/** Complete: sort → SHA256 → RSA sign → Base64 */
public static function sign($body, $privateKeyPem) {
$sorted = self::jsonToSortedString($body);
$hash = self::stringToSHA256($sorted);
return self::signWithPrivateKey($privateKeyPem, $hash);
}
/** Verify callback: sort → SHA256 → decrypt sign → compare */
public static function verifyCallback($body, $signHeader, $privateKeyPem) {
$sorted = self::jsonToSortedString($body);
$hash = self::stringToSHA256($sorted);
$decrypted = self::decryptWithPrivateKey($privateKeyPem, $signHeader);
return $hash === $decrypted;
}
}
import hashlib, base64, json
from Crypto.PublicKey import RSA
from Crypto.Signature import pkcs1_15
from Crypto.Hash import SHA256
from Crypto.Cipher import PKCS1_v1_5
def json_to_sorted_string(data: dict) -> str:
"""Sort params by key, join as key=value&key=value"""
return "&".join(
f"{k}={v}" for k, v in sorted(data.items())
if str(v) != ""
)
def string_to_sha256(s: str) -> str:
"""SHA-256 hex digest"""
return hashlib.sha256(s.encode()).hexdigest()
def sign_with_private_key(private_key_pem: str, message: str) -> str:
"""RSA PKCS1v15 sign -> Base64"""
key = RSA.import_key(private_key_pem)
h = SHA256.new(message.encode())
signature = pkcs1_15.new(key).sign(h)
return base64.b64encode(signature).decode()
def decrypt_with_private_key(private_key_pem: str, ciphertext_b64: str) -> str:
"""RSA PKCS1v15 decrypt -> plaintext"""
key = RSA.import_key(private_key_pem)
cipher = PKCS1_v1_5.new(key)
ciphertext = base64.b64decode(ciphertext_b64)
return cipher.decrypt(ciphertext, None).decode()
def generate_sign(body: dict, private_key_pem: str) -> str:
"""Complete: sort -> SHA256 -> RSA sign -> Base64"""
sorted_str = json_to_sorted_string(body)
hash_hex = string_to_sha256(sorted_str)
return sign_with_private_key(private_key_pem, hash_hex)
def verify_callback(body: dict, sign_header: str, private_key_pem: str) -> bool:
"""Verify callback: sort -> SHA256 -> decrypt sign -> compare"""
sorted_str = json_to_sorted_string(body)
hash_hex = string_to_sha256(sorted_str)
decrypted = decrypt_with_private_key(private_key_pem, sign_header)
return hash_hex == decrypted
import crypto from 'crypto';
function jsonToSortedString(data) {
return Object.keys(data).sort()
.filter(k => String(data[k]) !== "")
.map(k => `${k}=${data[k]}`)
.join("&");
}
function stringToSHA256(str) {
return crypto.createHash("sha256").update(str).digest("hex");
}
function signWithPrivateKey(privateKeyPem, message) {
const sign = crypto.createSign("SHA256");
sign.update(message);
return sign.sign(privateKeyPem, "base64");
}
function decryptWithPrivateKey(privateKeyPem, ciphertextB64) {
return crypto.privateDecrypt(
{ key: privateKeyPem, padding: crypto.constants.RSA_PKCS1_PADDING },
Buffer.from(ciphertextB64, "base64")
).toString();
}
function generateSign(body, privateKeyPem) {
const sorted = jsonToSortedString(body);
const hashHex = stringToSHA256(sorted);
return signWithPrivateKey(privateKeyPem, hashHex);
}
function verifyCallback(body, signHeader, privateKeyPem) {
const sorted = jsonToSortedString(body);
const hashHex = stringToSHA256(sorted);
const decrypted = decryptWithPrivateKey(privateKeyPem, signHeader);
return hashHex === decrypted;
}
Error Codes
All API responses include a code field. 0 indicates success.
| Code | Name | Description |
|---|---|---|
0 | Success | Request completed successfully |
7 | Failed | General failure |
101 | Merchant Not Found | Invalid or inactive merchant code |
102 | Insufficient Balance | Merchant balance too low for payout |
103 | Signature Error | RSA signature verification failed |
404 | Not Found | Resource not found |
4001 | Invalid Params | One or more parameters are invalid |
4002 | Missing Params | Required parameter is missing |
4003 | Invalid Format | Parameter format is incorrect |
5001 | Network Error | Upstream network issue |
5002 | Request Timeout | Upstream request timed out |
5003 | Service Unavailable | Service temporarily down |
9001 | Header Error | Missing or invalid request headers |
10001 | Order Error | General order processing error |
10002 | Order Not Found | Order does not exist |
10003 | Order Status Error | Invalid order status transition |
10004 | Order Closed | Order has been finalized |
Bank Codes
Supported banks and e-wallets for payout disbursements. Use the bankCode value in your payout request.
| bankCode | Short Name | Full Name |
|---|
Showing popular banks. Full list includes 150+ institutions. Contact support for the complete bank code table.
For AI Agents
Drop these docs straight into your AI coding assistant. Copy the machine-readable spec, paste it into ChatGPT, Claude, Cursor, or Copilot, and let it generate a working integration — signing, requests, and webhook handling included.
Grab the full machine-readable API reference below in one click.
ChatGPT, Claude, Cursor, Copilot, or any LLM with a context window.
Ask for signing, payin/payout calls, and webhook verification in your language.
“Open in …” copies the spec to your clipboard first, then opens the assistant — just paste.
Prompt starters
Copy a ready-made prompt, then paste it right after the spec.
Machine-readable spec
YAML · paste into any LLM context window
# PaymentAggregationSystem Payment API — Machine-Readable Reference
# Version: 1.0 | Format: YAML
# Usage: Paste this entire block into your AI agent / LLM context window.
api:
base_url: https://{domain}/api/v1
auth:
type: rsa2048_sha256
headers:
MCODE: "your_MCODE"
SIGN: "<base64_rsa_signature>"
Content-Type: application/json
signature_steps:
- sort request body keys alphabetically
- concatenate non-empty values as "key=value" joined by "&"
- compute SHA-256 hex digest of that string
- sign digest with RSA-2048 PKCS#1 v1.5 private key
- base64-encode the signature → SIGN header
endpoints:
- id: create_payin
method: POST
path: /payin/create
content_type: application/json
description: Create a collection (payin) order
params:
- { name: uid, type: string, required: true, desc: "User identifier" }
- { name: merchantOid, type: string, required: true, desc: "Unique merchant order ID (UUID recommended)" }
- { name: amount, type: integer, required: true, desc: "Amount in cents (e.g. 1000000 = IDR 10,000)" }
- { name: notifyUrl, type: string, required: true, desc: "Webhook URL for async notifications" }
- { name: redirectUrl, type: string, required: true, desc: "Redirect URL after payment" }
- { name: timestamp, type: integer, required: true, desc: "Unix timestamp (seconds)" }
- { name: name, type: string, required: true, desc: "Customer name" }
- { name: email, type: string, required: true, desc: "Customer email" }
- { name: phone, type: string, required: true, desc: "Customer phone" }
- { name: passage, type: string, required: true, desc: "Payment channel code. Obtain available pay-in codes from Admin > Channel Info > Payment Code." }
- { name: type, type: string, required: true, desc: 'Fixed value: "payin"' }
response:
- { name: code, type: integer, desc: "0 = success" }
- { name: data.oid, type: string, desc: "System order ID" }
- { name: data.merchantOid,type: string, desc: "Echo of your merchant order ID" }
- { name: data.url, type: string, desc: "Cashier page URL — redirect user here" }
- { name: msg, type: string, desc: "Human-readable message" }
- id: create_payout
method: POST
path: /payout/create
content_type: application/json
description: Create a disbursement (payout) order
params:
- { name: uid, type: string, required: true, desc: "User identifier" }
- { name: name, type: string, required: true, desc: "Beneficiary name" }
- { name: merchantOid, type: string, required: true, desc: "Unique merchant order ID" }
- { name: amount, type: integer, required: true, desc: "Amount in cents (e.g. 1000000 = IDR 10,000)" }
- { name: bankCode, type: string, required: true, desc: "Bank code (see bank_codes)" }
- { name: accountNo, type: string, required: true, desc: "Beneficiary account number" }
- { name: email, type: string, required: false, desc: "Beneficiary email" }
- { name: notifyUrl, type: string, required: true, desc: "Webhook URL for status updates" }
- { name: note, type: string, required: false, desc: "Remark / description" }
- { name: feeType, type: string, required: true, desc: '"Additional" (fee deducted from merchant balance) or "Include" (fee deducted from the player order amount)' }
- { name: passage, type: string, required: true, desc: "Payment channel code. Obtain available payout codes from Admin > Channel Info > Payment Code." }
- { name: type, type: string, required: true, desc: 'Fixed value: "payout"' }
response:
- { name: code, type: integer, desc: "0 = success" }
- { name: data.oid, type: string, desc: "System order ID" }
- { name: data.merchantOid,type: string, desc: "Echo of your merchant order ID" }
- { name: msg, type: string, desc: "Human-readable message" }
- id: query_order
method: POST
path: /query/order
content_type: application/json
description: Query order status (payin or payout)
params:
- { name: type, type: string, required: true, desc: '"payin" or "payout"' }
- { name: oid, type: string, required: false, desc: "System order ID (provide oid or merchantOid)" }
- { name: merchantOid, type: string, required: false, desc: "Your merchant order ID (provide oid or merchantOid)" }
response:
- { name: code, type: integer, desc: "0 = success" }
- { name: data.amount, type: integer, desc: "Order amount (cents)" }
- { name: data.merchantOid, type: string, desc: "Merchant order ID" }
- { name: data.orderOid, type: string, desc: "System order ID" }
- { name: data.timestamp, type: integer, desc: "Unix timestamp" }
- { name: data.status, type: string, desc: "Payin: order_success=paid | order_fail=failed | order_await=in progress | order_confirm=order created, method selected, cashier not opened | order_create=order created, method not selected, cashier not opened | order_over=timed out (sandbox mode only; production orders stay order_await indefinitely). Payout: order_success=paid | order_fail=failed | order_await=in progress | order_reverse=reversed after success" }
- { name: data.settleStatus, type: string, desc: "settle_success / settle_await / settle_failed / settle_unknown (payin only)" }
- { name: msg, type: string, desc: "Operation successful" }
- id: query_balance
method: GET
path: /query/balance
description: Query merchant balance (sign the MCODE value instead of body)
params: []
response:
- { name: code, type: integer, desc: "0 = success" }
- { name: data.amount, type: integer, desc: "Available balance (cents)" }
- { name: data.settleAmount, type: integer, desc: "Pending settlement (cents)" }
- { name: data.freezeAmount, type: integer, desc: "Frozen balance (cents)" }
- { name: msg, type: string, desc: "Human-readable message" }
webhooks:
- event: payin_callback
description: Sent when a payin order is paid or settled
headers:
ONEPAY-SIGN: RSA-encrypted hash (server encrypts body hash with your public key; decrypt with your private key to verify)
ONEPAY-MCODE: Merchant code
fields:
- { name: amount, type: integer, desc: "Order amount (cents)" }
- { name: merchantOid, type: string, desc: "Your merchant order ID" }
- { name: orderOid, type: string, desc: "System order ID" }
- { name: timestamp, type: integer, desc: "Unix timestamp" }
- { name: status, type: string, desc: "order_success=paid | order_fail=failed | order_await=in progress | order_confirm=order created, method selected, cashier not opened | order_create=order created, method not selected, cashier not opened | order_over=timed out (sandbox mode only; production orders stay order_await indefinitely)" }
- { name: settleStatus, type: string, desc: "settle_success / settle_await / settle_failed / settle_unknown" }
expected_response: 'plain text "SUCCESS" with HTTP 200'
- event: payout_callback
description: Sent when a payout order status changes
headers:
ONEPAY-SIGN: RSA-encrypted hash (server encrypts body hash with your public key; decrypt with your private key to verify)
ONEPAY-MCODE: Merchant code
fields:
- { name: amount, type: integer, desc: "Order amount (cents)" }
- { name: merchantOid, type: string, desc: "Your merchant order ID" }
- { name: orderOid, type: string, desc: "System order ID" }
- { name: timestamp, type: integer, desc: "Unix timestamp" }
- { name: status, type: string, desc: "order_success=paid | order_fail=failed | order_await=in progress | order_reverse=reversed after success" }
expected_response: 'plain text "SUCCESS" with HTTP 200'
error_codes:
- { code: 0, name: Success, desc: "Request completed successfully" }
- { code: 7, name: Failed, desc: "General failure" }
- { code: 101, name: Merchant Not Found, desc: "Invalid or inactive merchant code" }
- { code: 102, name: Insufficient Balance, desc: "Balance too low for payout" }
- { code: 103, name: Signature Error, desc: "RSA signature verification failed" }
- { code: 404, name: Not Found, desc: "Resource not found" }
- { code: 4001, name: Invalid Params, desc: "One or more parameters are invalid" }
- { code: 4002, name: Missing Params, desc: "Required parameter is missing" }
- { code: 4003, name: Invalid Format, desc: "Parameter format is incorrect" }
- { code: 5001, name: Network Error, desc: "Upstream network issue" }
- { code: 5002, name: Request Timeout, desc: "Upstream request timed out" }
- { code: 5003, name: Service Unavailable, desc: "Service temporarily down" }
- { code: 9001, name: Header Error, desc: "Missing or invalid request headers" }
- { code: 10001, name: Order Error, desc: "General order processing error" }
- { code: 10002, name: Order Not Found, desc: "Order does not exist" }
- { code: 10003, name: Order Status Error, desc: "Invalid order status transition" }
- { code: 10004, name: Order Closed, desc: "Order has been finalized" }
example:
description: "Create a payin order (curl)"
curl: |
curl -X POST https://{domain}/api/v1/payin/create \
-H "Content-Type: application/json" \
-H "MCODE: your_MCODE" \
-H "SIGN: <base64_signature>" \
-d '{
"uid": "user_001",
"merchantOid": "9d6e7671-64b9-408b-abad-1ef4601af101",
"amount": 1000000,
"notifyUrl": "https://yoursite.com/webhook/payin",
"redirectUrl": "https://yoursite.com/payment/done",
"timestamp": 1745723300,
"name": "John Doe",
"email": "[email protected]",
"phone": "08123456789",
"passage": "your_passage",
"type": "payin"
}'