
QRIS (Money Out)
This page provides information about the API endpoint for triggering a payment credit transaction using QRIS Money Out.
Here are the details of the API endpoint for triggering a payment credit transaction using QRIS Money Out:
| Method | Path | Format | Authentication |
|---|---|---|---|
| POST | /api/v2.0/qris/issuer/mpm/payment-credit | json | OAuth 2.0 with Access Token |
| Field | Value | Type | Mandatory | Length | Description | Example |
|---|---|---|---|---|---|---|
| X-PARTNER-ID | api_key | Alphanumeric | Mandatory | API key obtained from the merchant dashboard. | b3ed7d4b-a96c-6c08-b3c7-12c3124242d9 | |
| Accept | application/json | Alphabetic | Mandatory | Specifies JSON as the expected response format. | application/json | |
| Authorization | Bearer {bearerToken} | Alphanumeric | Mandatory | Bearer token obtained from the get access token endpoint. | Bearer eyJ0eXAiOiJKV1… | |
| X-Signature | HMAC-SHA512 Signature | Alphanumeric | Mandatory | HMAC-SHA512 signature for request authentication. See signature generation guide below. | a1b2c3d4e5f6… | |
| X-Timestamp | Unix Timestamp (seconds) | Numeric | Mandatory | Request timestamp in Unix seconds format | 1714618220 |
The request to the API will be in JSON format and will contain the following fields:
| Key | Data Type | Mandatory | Description |
|---|---|---|---|
| account_id | String | Yes | The unique internal identifier for the user/customer account (often a ULID or UUID). |
| reference_number | String | Yes | A unique merchant-side transaction ID used to link this payment request to an order in your system. |
| amount | String | Yes | The specific monetary value requested for the transaction (e.g., “11000.00”). |
| qr_data | String | Yes | The raw EMVCo string. This is the actual data encoded inside the QR code image. |
| customer_name | String | Yes | The full name of the customer associated with the payment account. |
| customer_email | String | No | The registered email address used for sending digital receipts or notifications. |
| customer_phone | String | No | The mobile number of the customer, usually formatted with the country code (e.g., 0821…). |
| customer_location | String | No | The city or geographic region of the customer during the transaction. |
{
"account_id": "01K5G4FZZ18DMK0M5QTR8Y9QY9",
"reference_number": "735463554",
"amount": "11000.00",
"qr_data": "hQVDUFYwMWFjTwegAAAGAiAgUAdRUklTQ1BNXyAJVGVzdCBVc2VyXy0EaWRlbloKk2B4OTQRIQAGL19QC3NpbmdhcGF5LmlknyUCAGJjHZ90GjAxSks3RlZHU041UlI4Vks0ODhDWUVXUlQz",
"customer_name": "Dhany Nurdiansyah",
"customer_email": "dhany.nurdiansyah@singapay.id",
"customer_phone": "082199711111",
"customer_location": "Tangerang"
}
The response from the API will be in JSON format and will contain the following fields:
| Key | Data Type | Mandatory | Description |
|---|---|---|---|
| response_code | String | Yes | Response code (see Response Code appendix) |
| response_message | String | Yes | Response message (see Response Code appendix) |
| data | Object | No | |
| > transaction_id | String | Yes | A unique identifier generated by system. |
| > reference_number | String | Yes | A unique identifier generated by merchant. |
| > transaction_status | Object | Yes | An object containing the current state of the payment. |
| >> code | String | Yes | The status code (e.g., “05”). |
| >> desc | String | Yes | The human-readable status (e.g., “Canceled”, “Success”, or “Pending”). |
| > qr_data | String | Yes | The raw EMVCo string. This is the actual data encoded inside the QR code image. |
| > type | String | Yes | The QRIS model used. cpm, mpm, mpm-dynamic, mpm_static where the merchant shows a QR unique to that bill. |
| > scope | String | Yes | The operational scope (e.g., “issuer”), indicating the entity processing the request. |
| > post_timestamp | String | Yes | The Unix timestamp (in milliseconds) when the transaction was first created/posted. |
| > processed_timestamp | String | Yes | The Unix timestamp (in milliseconds) when the transaction reached its final status. |
| > net_amount | Object | No | The Final Amount received by the merchant after deducting the fees. |
| >> value | String | Yes | Net amount for the transaction |
| >> currency | String | Yes | Currency of the transaction |
| > fee | Object | No | The transaction fee or service charge deducted from the transaction. |
| >> value | String | Yes | Fee amount for the transaction |
| >> currency | String | Yes | Currency of the transaction |
| > gross_amount | Object | No | The Total Amount paid by the customer (Net + Fee). |
| >> value | String | Yes | Gross amount for the transaction |
| >> currency | String | Yes | Currency of the transaction |
Success: Here’s an example of a successful response.
{
"response_code": "SP000",
"response_message": "Successfully",
"data": {
"transaction_id": "1012026051810442821994645",
"reference_number": "1111111120",
"transaction_status": {
"code": "03",
"desc": "Pending"
},
"post_timestamp": "1779075868000",
"processed_timestamp": "",
"qr_data": "00020101021226590013COM.GOJEK.WWW011893600914099765009502099976500950303UKE51370014ID.CO.QRIS.WWW0215ID10269976500955204821153033605405110005802ID5916Somay Mangga Dua6009TANGERANG61051533162360125LIESMenG11ROo0AcutD6cRS6J0703A0163046775",
"type": "mpm-dynamic",
"scope": "issuer",
"gross_amount": {
"currency": "IDR",
"value": "11077"
},
"fee": {
"currency": "IDR",
"value": "77"
},
"net_amount": {
"currency": "IDR",
"value": "11000.00"
},
"balance_after": {
"currency": "IDR",
"value": "0"
}
}
}
Error: Here’s an example of a validation error response.
{
"response_code": "SP117",
"response_message": "Beneficiary Account Not Found",
"data": {
"account_id": "01K5G4FZZ18DMK0M5QTR8Y9QY9",
"reference_number": "735463554",
"amount": "11000.00",
"qr_data": "hQVDUFYwMWFjTwegAAAGAiAgUAdRUklTQ1BNXyAJVGVzdCBVc2VyXy0EaWRlbloKk2B4OTQRIQAGL19QC3NpbmdhcGF5LmlknyUCAGJjHZ90GjAxSks3RlZHU041UlI4Vks0ODhDWUVXUlQz",
"customer_name": "Dhany Nurdiansyah",
"customer_email": "dhany.nurdiansyah@singapay.id",
"customer_phone": "082199711111",
"customer_location": "Tangerang"
}
}
Every transaction request must include an X-Signature header that will be validated by SingaPay. If the signature is invalid, the request will be rejected.
The signature uses HMAC-SHA512 algorithm with the following steps:
Sort all JSON object keys recursively in alphabetical order, then encode using:
JSON_UNESCAPED_UNICODE - Don’t escape unicode charactersJSON_UNESCAPED_SLASHES - Don’t escape forward slashesExample:
// Original body (key order doesn't matter)
{
"notes": "bayar cicilan",
"amount": 50000,
"bank_swift_code": "BRINIDJA",
"reference_number": "123456789123",
"bank_account_number": "521398319083210"
}
// Normalized (keys sorted alphabetically)
{"amount":50000,"bank_account_number":"521398319083210","bank_swift_code":"BRINIDJA","notes":"bayar cicilan","reference_number":"123456789123"}
Hash the normalized JSON string using SHA-256:
hashedBody = SHA256(normalizedBody)
Example:
hashedBody = "a1b2c3d4e5f6789..."
Combine the following components with colon (:) separator:
stringToSign = METHOD:ENDPOINT:ACCESS_TOKEN:HASHED_BODY:TIMESTAMP
Where:
METHOD = HTTP method (e.g., POST)ENDPOINT = Request URI path (e.g., /api/v1.0/disbursements/{account_id}/transfer)ACCESS_TOKEN = Bearer token from Authorization header (without “Bearer ” prefix)HASHED_BODY = SHA-256 hash from Step 2TIMESTAMP = Unix timestamp in seconds (from X-Timestamp header)Example:
POST:/api/v1.0/disbursements/abc123/transfer:eyJ0eXAiOiJKV1QiLCJhbGc...:a1b2c3d4e5f6789...:1714618220
Generate the signature using HMAC-SHA512 with your client_secret as the key:
signature = HMAC_SHA512(stringToSign, client_secret)
The output is a hexadecimal string (not base64).
<?php
function generateSignature($method, $endpoint, $accessToken, $body, $timestamp, $clientSecret) {
// Step 1: Normalize body
$bodyArray = json_decode($body, true);
ksort($bodyArray);
array_walk_recursive($bodyArray, function(&$item) {
if (is_array($item)) ksort($item);
});
$normalizedBody = json_encode($bodyArray, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
// Step 2: Hash normalized body
$hashedBody = hash('sha256', $normalizedBody);
// Step 3: Build string to sign
$stringToSign = "{$method}:{$endpoint}:{$accessToken}:{$hashedBody}:{$timestamp}";
// Step 4: Generate HMAC-SHA512 signature
$signature = hash_hmac('sha512', $stringToSign, $clientSecret);
return $signature;
}
// Usage
$signature = generateSignature(
'POST',
'/api/v1.0/disbursements/abc123/transfer',
'eyJ0eXAiOiJKV1QiLCJhbGc...',
'{"amount":50000,"bank_account_number":"521398319083210","bank_swift_code":"BRINIDJA","notes":"bayar cicilan","reference_number":"123456789123"}',
'1714618220',
'your_client_secret_here'
);
?>
const crypto = require('crypto');
function sortObjectKeys(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
if (Array.isArray(obj)) return obj.map(sortObjectKeys);
return Object.keys(obj)
.sort()
.reduce((sorted, key) => {
sorted[key] = sortObjectKeys(obj[key]);
return sorted;
}, {});
}
function generateSignature(method, endpoint, accessToken, body, timestamp, clientSecret) {
// Step 1: Normalize body
const bodyObject = JSON.parse(body);
const sortedBody = sortObjectKeys(bodyObject);
const normalizedBody = JSON.stringify(sortedBody);
// Step 2: Hash normalized body
const hashedBody = crypto
.createHash('sha256')
.update(normalizedBody)
.digest('hex');
// Step 3: Build string to sign
const stringToSign = `${method}:${endpoint}:${accessToken}:${hashedBody}:${timestamp}`;
// Step 4: Generate HMAC-SHA512 signature
const signature = crypto
.createHmac('sha512', clientSecret)
.update(stringToSign)
.digest('hex');
return signature;
}
// Usage
const signature = generateSignature(
'POST',
'/api/v1.0/disbursements/abc123/transfer',
'eyJ0eXAiOiJKV1QiLCJhbGc...',
'{"amount":50000,"bank_account_number":"521398319083210","bank_swift_code":"BRINIDJA","notes":"bayar cicilan","reference_number":"123456789123"}',
'1714618220',
'your_client_secret_here'
);
import hashlib
import hmac
import json
def sort_dict_recursive(obj):
if isinstance(obj, dict):
return {k: sort_dict_recursive(v) for k, v in sorted(obj.items())}
elif isinstance(obj, list):
return [sort_dict_recursive(item) for item in obj]
return obj
def generate_signature(method, endpoint, access_token, body, timestamp, client_secret):
# Step 1: Normalize body
body_dict = json.loads(body)
sorted_body = sort_dict_recursive(body_dict)
normalized_body = json.dumps(sorted_body, ensure_ascii=False, separators=(',', ':'))
# Step 2: Hash normalized body
hashed_body = hashlib.sha256(normalized_body.encode()).hexdigest()
# Step 3: Build string to sign
string_to_sign = f"{method}:{endpoint}:{access_token}:{hashed_body}:{timestamp}"
# Step 4: Generate HMAC-SHA512 signature
signature = hmac.new(
client_secret.encode(),
string_to_sign.encode(),
hashlib.sha512
).hexdigest()
return signature
# Usage
signature = generate_signature(
'POST',
'/api/v1.0/disbursements/abc123/transfer',
'eyJ0eXAiOiJKV1QiLCJhbGc...',
'{"amount":50000,"bank_account_number":"521398319083210","bank_swift_code":"BRINIDJA","notes":"bayar cicilan","reference_number":"123456789123"}',
'1714618220',
'your_client_secret_here'
)
/api/v1.0/... and include the {account_id} parameterclient_secret from merchant credentials, NOT your client_id or api_keySingaPay will:
X-PARTNER-ID matches an existing API keyclient_secret from the credentials tablehash_equals) to prevent timing attacks