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 GooglePay, ApplePay, and CashApp. Hosted cashier page with configurable redirect.
Payout (Disbursement)
Send funds to eCashapp, PayPal, Card, ACH, and Chime. 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-SHA256 signature of the sorted request parameters digest (Base64 encoded). See Signature Generation below. |
MCODE |
string | Your merchant code (e.g. MT262603YC3HBn) |
Signature Generation
key=value pairs joined by &// Sorted parameter string:
amount=1000&email=t.wcvji@ynnid.bn&merchantOid=7438bb79-...&name=test1¬ifyUrl=http://...&phone=12025551234&redirectUrl=https://...×tamp=12312311&type=payin&uid=28835218241114223x
// SHA-256 hash of sorted string:
383fb0663e87ebae7c5e1debb68746eab0f73a16ec82be3d5ca388bec0254691
// RSA-SHA256-signed (SHA256withRSA on the hash above, Base64 encoded):
a/WAwsxJQvYbfN18DHmK93RQbBQAQXLpETCBzrkK8a1cy+6M9BDX...=
SIGN and MCODE response headers. The SIGN value is the SHA-256 digest of the sorted response body, encrypted with your RSA public key. You can decrypt it with your private key to verify the response is authentic — the decrypted value should match the SHA-256 hash you compute from the sorted response body fields.
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: MT262603YC3HBn" \
-d '{
"uid": "3800",
"merchantOid": "9d6e7671-64b9-408b-abad-1ef4601af101",
"amount": 1000,
"notifyUrl": "https://your-server.com/callback/payin",
"redirectUrl": "https://your-app.com/payment/success",
"timestamp": 1745723235,
"name": "Taylor",
"email": "taylor@example.com",
"phone": "12025551234",
"passage": "testcash",
"type": "payin"
}'
import crypto from 'crypto';
const createPayin = async () => {
const body = {
uid: "3800",
merchantOid: crypto.randomUUID(),
amount: 1000,
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: "12025551234",
passage: "testcash",
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": "MT262603YC3HBn"
},
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": 1000,
"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": "12025551234",
"passage": "testcash",
"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", "MT262603YC3HBn")
}
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": "3800",
"merchantOid": str(uuid.uuid4()),
"amount": 1000,
"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": "12025551234",
"passage": "testcash",
"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": "MT262603YC3HBn"
}
)
return resp.json()
import java.security.*;
import java.util.*;
public class PagalPay {
public static String createPayin() throws Exception {
Map<String, Object> body = new TreeMap<>();
body.put("uid", "3800");
body.put("merchantOid", UUID.randomUUID().toString());
body.put("amount", 1000);
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", "12025551234");
body.put("passage", "testcash");
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, "MT262603YC3HBn");
}
}
$body = [
"uid" => "3800",
"merchantOid" => vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex(random_bytes(16)), 4)),
"amount" => 1000,
"notifyUrl" => "https://your-server.com/callback/payin",
"redirectUrl" => "https://your-app.com/payment/success",
"timestamp" => time(),
"name" => "Taylor", "email" => "taylor@example.com",
"phone" => "12025551234", "passage" => "testcash", "type" => "payin"
];
$sign = PagalPay::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. 1000 = USD 10) |
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. If set, skips channel selection on cashier. Test merchants can use: testcash (Cash App), testapple (Apple Pay), testgoogle (Google Pay) |
type | string | Yes | Fixed: "payin" |
Response
| Field | Type | Description |
|---|---|---|
code | integer | 0 on success |
data.oid | string | PagalPay 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_MERCHANT_CODE" \
-d '{
"uid": "3800",
"merchantOid": "9d6e7671-64b9-408b-abad-1ef4601af101",
"amount": 1000,
"notifyUrl": "https://your-server.com/callback/payin",
"redirectUrl": "https://your-app.com/payment/success",
"timestamp": 1745723235,
"name": "Taylor",
"email": "taylor@example.com",
"phone": "12025551234",
"passage": "testcash",
"type": "payin"
}'
const createPayin = async () => {
const body = {
uid: "3800",
merchantOid: crypto.randomUUID(),
amount: 1000,
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: "12025551234", passage: "testcash", 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": "3800", "merchantOid": uuid.New().String(),
"amount": 1000, "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": "12025551234",
"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": "3800", "merchantOid": str(uuid.uuid4()),
"amount": 1000, "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": "12025551234",
"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", "3800");
body.put("merchantOid", UUID.randomUUID().toString());
body.put("amount", 1000);
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", "12025551234"); 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" => "3800",
"merchantOid" => vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex(random_bytes(16)), 4)),
"amount" => 1000,
"notifyUrl" => "https://your-server.com/callback/payin",
"redirectUrl" => "https://your-app.com/payment/success",
"timestamp" => time(),
"name" => "Taylor", "email" => "taylor@example.com",
"phone" => "12025551234", "passage" => "testcash", "type" => "payin"
];
$sign = PagalPay::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. 1000 = USD 10) |
bankCode | string | Yes | Payment channel code. Supported values: ecashapp, paypal, card, ach, chime (see Bank Codes) |
accountNo | string | Yes | Beneficiary account. Rules vary by bankCode: • ecashapp / chime: CashTag starting with $, 3–20 chars, case-sensitive (e.g. $YourUsername)• paypal: Recipient email (e.g. xxxx@xxx.com)• card: Card number (e.g. 4111111111111111)• ach: Bank account number, 4–17 digits (e.g. 123456789012) |
cardValid | string | Conditional | Required when bankCode is card, omit for all other channels. Card expiry date, format MM/YYYY (e.g. 01/2028) |
routingNumber | string | Conditional | Required when bankCode is ach, omit for all other channels. US ACH routing number (9 digits) |
notifyUrl | string | Yes | Webhook URL for status updates |
note | string | No | Remark / description |
feeType | string | Yes | "Additional" (fee on top) or "Include" (fee deducted from amount) |
passage | string | Yes | Payment channel code. Test merchants can use testpayout |
type | string | Yes | Fixed: "payout" |
Code Examples
# note: optional | cardValid: required when bankCode=card | routingNumber: required when bankCode=ach
curl -X POST https://{domain}/api/v1/payout/create \
-H "Content-Type: application/json" \
-H "SIGN: <your_rsa_signature>" \
-H "MCODE: YOUR_MERCHANT_CODE" \
-d '{
"uid": "3800",
"name": "John Doe",
"notifyUrl": "https://your-server.com/callback/payout",
"amount": 1000,
"merchantOid": "d27eb86d-ada8-43cb-9d31-fa87dd578ab7",
"bankCode": "ecashapp",
"accountNo": "$YourUsername",
"note": "",
"type": "payout",
"feeType": "Additional",
"passage": "testpayout",
"cardValid": "",
"routingNumber": ""
}'
const body = {
uid: "3800", name: "John Doe",
notifyUrl: "https://your-server.com/callback/payout",
amount: 1000, merchantOid: crypto.randomUUID(),
bankCode: "ecashapp", accountNo: "$YourUsername",
note: "", // optional
type: "payout", feeType: "Additional",
passage: "testpayout",
cardValid: "", // required when bankCode=card
routingNumber: "" // required when bankCode=ach
};
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)
});
body := map[string]interface{}{
"uid": "3800", "name": "John Doe",
"notifyUrl": "https://your-server.com/callback/payout",
"amount": 1000, "merchantOid": uuid.New().String(),
"bankCode": "ecashapp", "accountNo": "$YourUsername",
"note": "", // optional
"type": "payout", "feeType": "Additional",
"passage": "testpayout",
"cardValid": "", // required when bankCode=card
"routingNumber": "", // required when bankCode=ach
}
// sign and POST as shown in payin example
body = {
"uid": "3800", "name": "John Doe",
"notifyUrl": "https://your-server.com/callback/payout",
"amount": 1000, "merchantOid": str(uuid.uuid4()),
"bankCode": "ecashapp", "accountNo": "$YourUsername",
"note": "", # optional
"type": "payout", "feeType": "Additional",
"passage": "testpayout",
"cardValid": "", # required when bankCode=card
"routingNumber": "" # required when bankCode=ach
}
# sign and POST as shown in payin example
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", 1000); body.put("merchantOid", UUID.randomUUID().toString());
body.put("bankCode", "ecashapp"); body.put("accountNo", "$YourUsername");
body.put("note", ""); // optional
body.put("type", "payout"); body.put("feeType", "Additional");
body.put("passage", "testpayout");
body.put("cardValid", ""); // required when bankCode=card
body.put("routingNumber", ""); // required when bankCode=ach
// sign and POST as shown in payin example
$body = [
"uid" => "3800", "name" => "John Doe",
"notifyUrl" => "https://your-server.com/callback/payout",
"amount" => 1000, "merchantOid" => vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex(random_bytes(16)), 4)),
"bankCode" => "ecashapp", "accountNo" => "$YourUsername",
"note" => "", // optional
"type" => "payout", "feeType" => "Additional",
"passage" => "testpayout",
"cardValid" => "", // required when bankCode=card
"routingNumber" => "" // required when bankCode=ach
];
// sign and POST as shown in payin example
{
"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 | PagalPay 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_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://{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 = PagalPay::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);
{
"code": 0,
"data": {
"amount": 1000,
"merchantOid": "9d6e7671-64b9-408b-abad-1ef4601af101",
"orderOid": "CLN270425tR53wb2W",
"timestamp": 1745723300,
"status": "order_success",
"settleStatus": "settle_success"
},
"msg": "Operation successful"
}
{
"code": 0,
"data": {
"amount": 1000,
"merchantOid": "d27eb86d-ada8-43cb-9d31-fa87dd578ab7",
"orderOid": "PAY250325DoNhh8AY",
"timestamp": 1745723400,
"status": "order_success"
},
"msg": "Operation successful"
}
/query/balance
Query merchant account balance. For this endpoint, sign the SHA-256 hash of your MCODE value (no request body).
Response
| Field | Type | Description |
|---|---|---|
code | integer | Status code (0 = success) |
msg | string | Result message |
data.amount | integer | Available balance (cents) |
data.settleAmount | integer | Pending settlement balance (cents) |
data.freezeAmount | integer | Frozen balance (cents) |
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_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://{domain}/api/v1/query/balance", {
headers: { "SIGN": signB64, "MCODE": MCODE }
});
return res.json();
// { code: 0, msg: "success", data: { 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://{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, "msg": "success", "data": {"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://{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, "msg": "success", "data": {"amount": 50000000, "settleAmount": 10000000, "freezeAmount": 0}}
{
"code": 0,
"msg": "success",
"data": {
"amount": 50000000,
"settleAmount": 10000000,
"freezeAmount": 0
}
}
Webhooks
PagalPay sends asynchronous notifications to your notifyUrl when order status changes. You must respond with the string SUCCESS, otherwise PagalPay will retry up to 5 times.
SIGN header. Use your RSA private key to decrypt the SIGN header — the decrypted value should match the SHA-256 hash you compute independently from the sorted callback body parameters. Ensure idempotent handling — the same notification may arrive multiple times.
{"Data":{...},"Header":{...}} for debugging purposes — this combines the HTTP body and headers into one log entry. The actual HTTP request sent to your notifyUrl has a flat JSON body (fields listed below) with MCODE and SIGN as separate HTTP request headers.
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 | PagalPay system order ID |
timestamp | integer | Unix timestamp |
status | string | Order status (see below) |
settleStatus | string | Settlement status (see below) |
order_success | Payment received |
order_fail | Payment failed |
order_await | Awaiting payment |
order_confirm | Payment method selected, cashier page not yet opened |
order_create | Order created, payment method not yet selected |
order_over | Order timed out (test channel only; live orders remain order_await) |
settle_success | Settled |
settle_await | Pending settlement |
settle_unknown | N/A (order not successful) |
{
"amount": 1000,
"merchantOid": "cf75c3a8-6028-4a62-9722-2cd5bc4b820c",
"orderOid": "CLN250325iFgO5Ibt",
"timestamp": 1742895347,
"status": "order_success",
"settleStatus": "settle_success"
}
# Verification steps:
# 1. Receive POST with headers SIGN and MCODE
# 2. Sort body params by key → concatenate as 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["sign"]; // Express lowercases all header names
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("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 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("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("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_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 |
|---|---|---|
amount | integer | Order amount (cents) |
merchantOid | string | Your merchant order ID |
orderOid | string | PagalPay system order ID |
timestamp | integer | Unix timestamp |
status | string | Order status (see below) |
order_success | Payment received |
order_fail | Payment failed |
order_await | Awaiting payment |
order_reverse | Order reversed |
{
"amount": 1000,
"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["sign"]; // Express lowercases all header names
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("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("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("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_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 PagalPay
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 PagalPay {
/** 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 PagalPay {
/** 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
5 payout channels are supported. Use the corresponding bankCode value in your payout request.
| bankCode | Channel | accountNo Rules |
|---|
All supported payout channels are listed above. Contact support if you have any questions.
For AI Agents
Structured API reference for LLMs and coding assistants. Copy this block and paste into your AI agent's context window for automatic integration.
# PagalPay 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: "<merchant_code>" # e.g. MT262603YC3HBn
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. 1000 = USD 10)" }
- { 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. If set, skips channel selection on cashier. Test merchants can use: testcash (Cash App), testapple (Apple Pay), testgoogle (Google Pay)" }
- { name: type, type: string, required: true, desc: 'Fixed value: "payin"' }
response:
- { name: code, type: integer, desc: "0 = success" }
- { name: data.oid, type: string, desc: "PagalPay 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" }
- { name: bankCode, type: string, required: true, desc: "Payment channel code. Supported values: ecashapp, paypal, card, ach, chime" }
- { name: accountNo, type: string, required: true, desc: "Beneficiary account. ecashapp/chime: CashTag starting with $; paypal: recipient email; card: card number; ach: bank account number (4-17 digits)" }
- { name: cardValid, type: string, required: false, desc: "Required when bankCode=card. Card expiry date, format MM/YYYY (e.g. 01/2028)" }
- { name: routingNumber, type: string, required: false, desc: "Required when bankCode=ach. US ACH routing number (9 digits)" }
- { 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 on top) or "Include" (fee deducted)' }
- { name: passage, type: string, required: true, desc: "Payment channel code. Test merchants can use testpayout" }
- { name: type, type: string, required: true, desc: 'Fixed value: "payout"' }
response:
- { name: code, type: integer, desc: "0 = success" }
- { name: data.oid, type: string, desc: "PagalPay 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: "PagalPay 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: "PagalPay order ID" }
- { name: data.timestamp, type: integer, desc: "Unix timestamp" }
- { name: data.status, type: string, desc: "payin: order_success / order_fail / order_await / order_confirm / order_create / order_over; payout: order_success / order_fail / order_await / order_reverse" }
- { name: data.settleStatus, type: string, desc: "settle_success / settle_await / settle_unknown (payin only)" }
- { name: msg, type: string, desc: "Human-readable message" }
- 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: amount, type: integer, desc: "Available balance (cents)" }
- { name: settleAmount, type: integer, desc: "Pending settlement (cents)" }
- { name: freezeAmount, type: integer, desc: "Frozen balance (cents)" }
webhooks:
- event: payin_callback
description: Sent when a payin order is paid or settled
headers:
SIGN: RSA signature (verify with your private key)
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: "PagalPay order ID" }
- { name: timestamp, type: integer, desc: "Unix timestamp" }
- { name: status, type: string, desc: "order_success / order_fail / order_await / order_confirm / order_create / order_over" }
- { name: settleStatus, type: string, desc: "settle_success / settle_await / settle_unknown" }
expected_response: 'plain text "SUCCESS" with HTTP 200'
- event: payout_callback
description: Sent when a payout order status changes
headers:
SIGN: RSA signature (verify with your private key)
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: "PagalPay order ID" }
- { name: timestamp, type: integer, desc: "Unix timestamp" }
- { name: status, type: string, desc: "order_success / order_fail / order_await / order_reverse" }
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: MT262603YC3HBn" \
-H "SIGN: <base64_signature>" \
-d '{
"uid": "user_001",
"merchantOid": "9d6e7671-64b9-408b-abad-1ef4601af101",
"amount": 1000,
"notifyUrl": "https://yoursite.com/webhook/payin",
"redirectUrl": "https://yoursite.com/payment/done",
"timestamp": 1745723300,
"name": "John Doe",
"email": "john@example.com",
"phone": "12025551234",
"type": "payin"
}'