All systems operational
Sign in Get API Keys
API v1 — Production Ready

Payment Infrastructure
for Southeast Asia

Integrate payin and payout capabilities with a single, unified API. RSA-secured, real-time webhooks, 150+ Indonesian banks supported.

4
API Endpoints
150+
Bank Codes
RSA
2048-bit Security
5
SDK Languages

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
ONEPAY-SIGN string RSA signature of the request body (Base64 encoded)
ONEPAY-MCODE string Your merchant code (e.g. MT182504LVhvaB)

Signature Generation

1
Sort parameters alphabetically by key
Concatenate non-empty values as key=value pairs joined by &
2
Hash with SHA-256
Compute SHA-256 hex digest of the sorted string
3
Sign with RSA private key
Sign the hash using PKCS#1 v1.5, then Base64 encode the result
Signature String Example
// Sorted parameter string:
amount=1000000&email=t.wcvji@ynnid.bn&merchantOid=7438bb79-...&name=test1¬ifyUrl=http://...&phone=18144528972&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://api.onepay.vip/api/v1/payin/create \
  -H "Content-Type: application/json" \
  -H "ONEPAY-SIGN: <your_rsa_signature>" \
  -H "ONEPAY-MCODE: MT182504LVhvaB" \
  -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": "taylor@example.com",
    "phone": "08123456789",
    "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: "taylor@example.com",
    phone: "08123456789",
    passage: "",
    type: "payin"
  };

  const sign = generateRSASignature(body, privateKey);

  const res = await fetch("https://api.onepay.vip/api/v1/payin/create", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "ONEPAY-SIGN": sign,
      "ONEPAY-MCODE": "MT182504LVhvaB"
    },
    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":        "taylor@example.com",
        "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://api.onepay.vip/api/v1/payin/create", body)
    req.Header.Set("ONEPAY-SIGN", signB64)
    req.Header.Set("ONEPAY-MCODE", "MT182504LVhvaB")
}
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": "taylor@example.com",
        "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://api.onepay.vip/api/v1/payin/create",
        json=body,
        headers={
            "ONEPAY-SIGN": sign_b64,
            "ONEPAY-MCODE": "MT182504LVhvaB"
        }
    )
    return resp.json()
import java.security.*;
import java.util.*;

public class OnePay {
    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", "taylor@example.com");
        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 ONEPAY-SIGN + ONEPAY-MCODE
        return httpPost("https://api.onepay.vip/api/v1/payin/create",
            body, signB64, "MT182504LVhvaB");
    }
}
$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" => "taylor@example.com",
    "phone" => "08123456789", "type" => "payin"
];
$sign = OnePay::sign($body, $privateKeyPem);

$ch = curl_init("https://api.onepay.vip/api/v1/payin/create");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        "Content-Type: application/json",
        "ONEPAY-SIGN: " . $sign,
        "ONEPAY-MCODE: " . $mcode
    ],
    CURLOPT_POSTFIELDS => json_encode($body),
    CURLOPT_RETURNTRANSFER => true
]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);
Response
{
  "code": 0,
  "data": {
    "oid": "CLN270425tR53wb2W",
    "merchantOid": "9d6e7671-64b9-408b-abad-1ef4601af101",
    "url": "https://cashier.onepay.vip/index.html?oid=CLN270425tR53wb2W"
  },
  "msg": "Operation successful"
}

API Reference

Base URL: https://api.onepay.vip/api/v1

POST /payin/create

Create a payin (collection) order. Returns a cashier URL for the user to complete payment.

Request Body

Field Type Required Description
uidstringYesUser identifier
merchantOidstringYesUnique merchant order ID (UUID recommended)
amountintegerYesAmount in cents (e.g. 1000000 = IDR 10,000)
notifyUrlstringYesWebhook URL for async notifications
redirectUrlstringYesRedirect URL after payment
timestampintegerYesUnix timestamp (seconds)
namestringYesCustomer name
emailstringYesCustomer email
phonestringYesCustomer phone
passagestringNoPayment channel code. If set, skips channel selection on cashier
typestringYesFixed: "payin"

Response

Field Type Description
codeinteger0 on success
data.oidstringOnePay system order ID
data.merchantOidstringYour merchant order ID (echo back)
data.urlstringCashier page URL — redirect the user here
msgstring"Operation successful"

Code Examples

curl -X POST https://api.onepay.vip/api/v1/payin/create \
  -H "Content-Type: application/json" \
  -H "ONEPAY-SIGN: <your_rsa_signature>" \
  -H "ONEPAY-MCODE: YOUR_MERCHANT_CODE" \
  -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": "taylor@example.com",
    "phone": "08123456789",
    "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: "taylor@example.com",
    phone: "08123456789", passage: "", type: "payin"
  };
  const sign = generateRSASignature(body, privateKey);
  const res = await fetch("https://api.onepay.vip/api/v1/payin/create", {
    method: "POST",
    headers: { "Content-Type": "application/json", "ONEPAY-SIGN": sign, "ONEPAY-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": "taylor@example.com", "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://api.onepay.vip/api/v1/payin/create", jsonBody)
req.Header.Set("ONEPAY-SIGN", signB64)
req.Header.Set("ONEPAY-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": "taylor@example.com", "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://api.onepay.vip/api/v1/payin/create",
    json=body, headers={"ONEPAY-SIGN": sign_b64, "ONEPAY-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", "taylor@example.com");
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 ONEPAY-SIGN + ONEPAY-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" => "taylor@example.com",
    "phone" => "08123456789", "type" => "payin"
];
$sign = OnePay::sign($body, $privateKeyPem);

$ch = curl_init("https://api.onepay.vip/api/v1/payin/create");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        "Content-Type: application/json",
        "ONEPAY-SIGN: " . $sign,
        "ONEPAY-MCODE: " . $mcode
    ],
    CURLOPT_POSTFIELDS => json_encode($body),
    CURLOPT_RETURNTRANSFER => true
]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);
Response
{
  "code": 0,
  "data": {
    "oid": "CLN270425tR53wb2W",
    "merchantOid": "9d6e7671-64b9-408b-abad-1ef4601af101",
    "url": "https://cashier.onepay.vip/index.html?oid=CLN270425tR53wb2W"
  },
  "msg": "Operation successful"
}
POST /payout/create

Create a payout (disbursement) order to send funds to a bank account or e-wallet.

Request Body

Field Type Required Description
uidstringYesUser identifier
namestringYesBeneficiary name
merchantOidstringYesUnique merchant order ID
amountintegerYesAmount in cents
bankCodestringYesBank code (see Bank Codes)
accountNostringYesBeneficiary account number
emailstringNoBeneficiary email
notifyUrlstringYesWebhook URL for status updates
notestringNoRemark / description
feeTypestringYes"Additional" (fee on top) or "Include" (fee deducted from amount)
passagestringYesPayment channel code
typestringYesFixed: "payout"

Code Examples

curl -X POST https://api.onepay.vip/api/v1/payout/create \
  -H "Content-Type: application/json" \
  -H "ONEPAY-SIGN: <your_rsa_signature>" \
  -H "ONEPAY-MCODE: YOUR_MERCHANT_CODE" \
  -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": "john@example.com",
    "note": "Withdrawal request",
    "type": "payout",
    "feeType": "Additional",
    "passage": "your_passage_code"
  }'
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: "john@example.com",
    note: "Withdrawal request",
    type: "payout",
    feeType: "Additional",
    passage: "your_passage_code"
  };
  const sign = generateRSASignature(body, privateKey);
  const res = await fetch("https://api.onepay.vip/api/v1/payout/create", {
    method: "POST",
    headers: { "Content-Type": "application/json", "ONEPAY-SIGN": sign, "ONEPAY-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":        "john@example.com",
    "note":         "Withdrawal request",
    "type":         "payout",
    "feeType":      "Additional",
    "passage":      "your_passage_code",
}
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://api.onepay.vip/api/v1/payout/create", jsonBody)
req.Header.Set("ONEPAY-SIGN", signB64)
req.Header.Set("ONEPAY-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": "john@example.com",
    "note": "Withdrawal request",
    "type": "payout",
    "feeType": "Additional",
    "passage": "your_passage_code"
}
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://api.onepay.vip/api/v1/payout/create",
    json=body, headers={"ONEPAY-SIGN": sign_b64, "ONEPAY-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", "john@example.com");
body.put("note", "Withdrawal request");
body.put("type", "payout");
body.put("feeType", "Additional");
body.put("passage", "your_passage_code");

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 ONEPAY-SIGN + ONEPAY-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" => "john@example.com",
    "note" => "Withdrawal request",
    "type" => "payout",
    "feeType" => "Additional",
    "passage" => "your_passage_code"
];
$sign = OnePay::sign($body, $privateKeyPem);

$ch = curl_init("https://api.onepay.vip/api/v1/payout/create");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        "Content-Type: application/json",
        "ONEPAY-SIGN: " . $sign,
        "ONEPAY-MCODE: " . $mcode
    ],
    CURLOPT_POSTFIELDS => json_encode($body),
    CURLOPT_RETURNTRANSFER => true
]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);
Response
{
  "code": 0,
  "data": {
    "oid": "PAY200325mb7gKfOI",
    "merchantOid": "d27eb86d-ada8-43cb-9d31-fa87dd578ab7"
  },
  "msg": "Operation successful"
}
POST /query/order

Query the status of a payin or payout order. Provide either oid or merchantOid.

Field Type Description
typestring"payin" or "payout"
oidstringOnePay system order ID (either oid or merchantOid)
merchantOidstringYour merchant order ID (either oid or merchantOid)

Code Examples

curl -X POST https://api.onepay.vip/api/v1/query/order \
  -H "Content-Type: application/json" \
  -H "ONEPAY-SIGN: <your_rsa_signature>" \
  -H "ONEPAY-MCODE: YOUR_MERCHANT_CODE" \
  -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://api.onepay.vip/api/v1/query/order", {
    method: "POST",
    headers: { "Content-Type": "application/json", "ONEPAY-SIGN": sign, "ONEPAY-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://api.onepay.vip/api/v1/query/order", jsonBody)
req.Header.Set("ONEPAY-SIGN", base64.StdEncoding.EncodeToString(sig))
req.Header.Set("ONEPAY-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://api.onepay.vip/api/v1/query/order",
        json=body, headers={"ONEPAY-SIGN": sign_b64, "ONEPAY-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 ONEPAY-SIGN + ONEPAY-MCODE
$body = [
    "type" => "payin",
    "merchantOid" => "9d6e7671-64b9-408b-abad-1ef4601af101"
];
$sign = OnePay::sign($body, $privateKeyPem);

$ch = curl_init("https://api.onepay.vip/api/v1/query/order");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        "Content-Type: application/json",
        "ONEPAY-SIGN: " . $sign,
        "ONEPAY-MCODE: " . $mcode
    ],
    CURLOPT_POSTFIELDS => json_encode($body),
    CURLOPT_RETURNTRANSFER => true
]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);
Response — same structure as the corresponding callback payload
// 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"
}
GET /query/balance

Query merchant account balance. For this endpoint, sign the ONEPAY-MCODE value itself.

Response

Field Type Description
codeintegerStatus code (0 = success)
amountintegerAvailable balance (cents)
settleAmountintegerPending settlement balance (cents)
freezeAmountintegerFrozen balance (cents)

Code Examples

# Note: ONEPAY-SIGN is the RSA signature of your MCODE value itself
curl -X GET https://api.onepay.vip/api/v1/query/balance \
  -H "ONEPAY-SIGN: <rsa_sign_of_mcode>" \
  -H "ONEPAY-MCODE: YOUR_MERCHANT_CODE"
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://api.onepay.vip/api/v1/query/balance", {
    headers: { "ONEPAY-SIGN": signB64, "ONEPAY-MCODE": MCODE }
  });
  return res.json();
  // { code: 0, amount: 50000000, settleAmount: 10000000, freezeAmount: 0 }
};
// 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://api.onepay.vip/api/v1/query/balance", nil)
req.Header.Set("ONEPAY-SIGN", signB64)
req.Header.Set("ONEPAY-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://api.onepay.vip/api/v1/query/balance",
        headers={"ONEPAY-SIGN": sign_b64, "ONEPAY-MCODE": MCODE})
    return resp.json()
    # {"code": 0, "amount": 50000000, "settleAmount": 10000000, "freezeAmount": 0}
// 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://api.onepay.vip/api/v1/query/balance").openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("ONEPAY-SIGN", signB64);
conn.setRequestProperty("ONEPAY-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://api.onepay.vip/api/v1/query/balance");
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER => [
        "ONEPAY-SIGN: " . $signB64,
        "ONEPAY-MCODE: " . $mcode
    ],
    CURLOPT_RETURNTRANSFER => true
]);
$result = json_decode(curl_exec($ch), true);
// {"code": 0, "amount": 50000000, "settleAmount": 10000000, "freezeAmount": 0}
Response
{
  "code": 0,
  "amount": 50000000,
  "settleAmount": 10000000,
  "freezeAmount": 0
}

Webhooks

OnePay sends asynchronous notifications to your notifyUrl when order status changes. You must respond with the string SUCCESS, otherwise OnePay will retry up to 5 times.

Verification: Use your RSA private key to decrypt the ONEPAY-SIGN header. The decrypted value should match the SHA-256 hash of the sorted callback 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
amountintegerOrder amount (cents)
merchantOidstringYour merchant order ID
orderOidstringOnePay system order ID
timestampintegerUnix timestamp
statusstringOrder status (see below)
settleStatusstringSettlement status (see below)
Order Status
order_successPayment received
order_failPayment failed
order_awaitIn progress
Settlement Status
settle_successSettled
settle_awaitPending settlement
settle_unknownN/A (order not successful)
Callback Payload
{
  "amount": 1000000,
  "merchantOid": "cf75c3a8-6028-4a62-9722-2cd5bc4b820c",
  "orderOid": "CLN250325iFgO5Ibt",
  "timestamp": 1742895347,
  "status": "order_success",
  "settleStatus": "settle_success"
}
Callback Handler
# Verification steps:
# 1. Receive POST with headers ONEPAY-SIGN and ONEPAY-MCODE
# 2. Sort body params by key → concatenate as key=value&key=value
# 3. SHA-256 hash the sorted string
# 4. Decrypt ONEPAY-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 params, hash, verify
  const sorted = Object.keys(body).sort()
    .filter(k => body[k] !== "")
    .map(k => `${k}=${body[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 (body.status === "order_success") {
    await updateOrderStatus(body.merchantOid, body.status, body.settleStatus);
  }
  res.send("SUCCESS");
});
func PayinCallback(c *gin.Context) {
    sign := c.GetHeader("ONEPAY-SIGN")
    var body map[string]interface{}
    c.ShouldBindJSON(&body)

    // Sort params, hash
    sorted := JsonToSortedString(body)
    hash := sha256.Sum256([]byte(sorted))
    hashHex := hex.EncodeToString(hash[:])

    // Decrypt ONEPAY-SIGN with private key
    decrypted, err := DecryptWithPrivateKey(privKey, sign)
    if err != nil || decrypted != hashHex {
        c.String(400, "FAIL")
        return
    }

    // Process order (idempotent)
    status := body["status"].(string)
    if status == "order_success" {
        updateOrderStatus(body["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 params, hash
    sorted_str = "&".join(f"{k}={v}" for k, v in sorted(body.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 body["status"] == "order_success":
        update_order(body["merchantOid"], body["status"], body["settleStatus"])
    return "SUCCESS"
// Spring Boot handler
@PostMapping("/callback/payin")
public String payinCallback(
    @RequestHeader("ONEPAY-SIGN") String sign,
    @RequestBody Map<String, Object> body) {

    // Sort params, hash
    String sorted = new TreeMap<>(body).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(body.get("status"))) {
        updateOrder(body.get("merchantOid"), body.get("status"));
    }
    return "SUCCESS";
}
// Laravel/raw PHP callback handler
$sign = $_SERVER['HTTP_ONEPAY_SIGN'] ?? '';
$body = json_decode(file_get_contents('php://input'), true);

// Sort params, hash
ksort($body);
$pairs = [];
foreach ($body 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 ($body['status'] === 'order_success') {
    updateOrder($body['merchantOid'], $body['status'], $body['settleStatus']);
}
echo "SUCCESS";

Payout Callback

Sent when a payout order status changes.

Field Type Description
amountintegerOrder amount (cents)
merchantOidstringYour merchant order ID
orderOidstringOnePay system order ID
timestampintegerUnix timestamp
statusstringorder_success / order_fail / order_await
Callback Payload
{
  "amount": 1500000,
  "merchantOid": "6a6ab062-9a35-4a07-87cd-5ca0492c338c",
  "orderOid": "PAY250325DoNhh8AY",
  "timestamp": 1742899401,
  "status": "order_success"
}
Callback Handler
# 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 ONEPAY-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 sorted = Object.keys(body).sort()
    .filter(k => body[k] !== "")
    .map(k => `${k}=${body[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 (body.status === "order_success") {
    await completeWithdrawal(body.merchantOid, body.amount);
  } else if (body.status === "order_fail") {
    await refundWithdrawal(body.merchantOid);
  }
  res.send("SUCCESS");
});
func PayoutCallback(c *gin.Context) {
    sign := c.GetHeader("ONEPAY-SIGN")
    var body map[string]interface{}
    c.ShouldBindJSON(&body)

    sorted := JsonToSortedString(body)
    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 := body["status"].(string)
    switch status {
    case "order_success":
        completeWithdrawal(body["merchantOid"].(string))
    case "order_fail":
        refundWithdrawal(body["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()

    sorted_str = "&".join(f"{k}={v}" for k, v in sorted(body.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 body["status"] == "order_success":
        complete_withdrawal(body["merchantOid"])
    elif body["status"] == "order_fail":
        refund_withdrawal(body["merchantOid"])
    return "SUCCESS"
@PostMapping("/callback/payout")
public String payoutCallback(
    @RequestHeader("ONEPAY-SIGN") String sign,
    @RequestBody Map<String, Object> body) {

    String sorted = new TreeMap<>(body).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 (body.get("status").toString()) {
        case "order_success": completeWithdrawal(body); break;
        case "order_fail":    refundWithdrawal(body);   break;
    }
    return "SUCCESS";
}
$sign = $_SERVER['HTTP_ONEPAY_SIGN'] ?? '';
$body = json_decode(file_get_contents('php://input'), true);

ksort($body);
$pairs = [];
foreach ($body 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 ($body['status']) {
    case 'order_success': completeWithdrawal($body); break;
    case 'order_fail':    refundWithdrawal($body);   break;
}
echo "SUCCESS";

Signature Utilities

Complete signing and verification implementations. Copy these utility functions into your project — no SDK installation required.

package onepay

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 OnePay {

    /** 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 OnePay {

    /** 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
0SuccessRequest completed successfully
7FailedGeneral failure
101Merchant Not FoundInvalid or inactive merchant code
102Insufficient BalanceMerchant balance too low for payout
103Signature ErrorRSA signature verification failed
404Not FoundResource not found
4001Invalid ParamsOne or more parameters are invalid
4002Missing ParamsRequired parameter is missing
4003Invalid FormatParameter format is incorrect
5001Network ErrorUpstream network issue
5002Request TimeoutUpstream request timed out
5003Service UnavailableService temporarily down
9001Header ErrorMissing or invalid request headers
10001Order ErrorGeneral order processing error
10002Order Not FoundOrder does not exist
10003Order Status ErrorInvalid order status transition
10004Order ClosedOrder has been finalized

Bank Codes

Supported banks and e-wallets for payout disbursements. Use the bankCode value in your payout request.

Code Short Name Full Name
014BCABank Central Asia (BCA)
009BNIBank Negara Indonesia (BNI)
002BRIBank Rakyat Indonesia (BRI)
008MANDIRIBank Mandiri
4510BSIBank Syariah Indonesia (BSI)
022CIMBBank CIMB Niaga
013PERMATABank Permata
011DANAMONBank Danamon
200BTNBank Tabungan Negara (BTN)
019PANINBank Panin
016MAYBANKBank Maybank
028OCBCBank OCBC NISP
023UOBTMRW/Bank UOB Indonesia
426MEGABank Mega
213BTPNBank BTPN
153SINARMASBank Sinarmas
046DBSBank DBS Indonesia
484HANALINE Bank/KEB Hana
542JAGOBank ARTOS/Bank Jago
535SEABANKSeabank/Bank Kesejahteraan Ekonomi
E-Wallets
10001OVOOVO
10002DANADANA
10003GOPAYGOPAY
10008SHOPEEPAYSHOPEEPAY
10009LINKAJALINKAJA

Showing popular banks. Full list includes 150+ institutions. Contact support for the complete bank code table.

Changelog

v1.3.0 2025-04-27 Latest
  • Added e-wallet support (OVO, DANA, GOPAY, ShopeePay, LinkAja)
  • Improved callback retry mechanism
v1.2.0 2025-03-25
  • Added settlement status tracking in payin callbacks
  • Introduced feeType parameter for payout orders
  • Balance query now returns settleAmount and freezeAmount
v1.0.0 2025-02-01
  • Initial release with payin, payout, order query, and balance query
  • RSA-2048 signature authentication
  • Support for 140+ Indonesian banks