Singapay Home Page
Logo Icon
  1. QRIS (Money Out)
  2. Trigger Payment Credit

Information

Here are the details of the API endpoint for triggering a payment credit transaction using QRIS Money Out:

MethodPathFormatAuthentication
POST/api/v2.0/qris/issuer/mpm/payment-creditjsonOAuth 2.0 with Access Token

Headers Structure

FieldValueTypeMandatoryLengthDescriptionExample
X-PARTNER-IDapi_keyAlphanumericMandatoryAPI key obtained from the merchant dashboard.b3ed7d4b-a96c-6c08-b3c7-12c3124242d9
Acceptapplication/jsonAlphabeticMandatorySpecifies JSON as the expected response format.application/json
AuthorizationBearer {bearerToken}AlphanumericMandatoryBearer token obtained from the get access token endpoint.Bearer eyJ0eXAiOiJKV1…
X-SignatureHMAC-SHA512 SignatureAlphanumericMandatoryHMAC-SHA512 signature for request authentication. See signature generation guide below.a1b2c3d4e5f6…
X-TimestampUnix Timestamp (seconds)NumericMandatoryRequest timestamp in Unix seconds format1714618220

Request

Request Structure

The request to the API will be in JSON format and will contain the following fields:

KeyData TypeMandatoryDescription
account_idStringYesThe unique internal identifier for the user/customer account (often a ULID or UUID).
reference_numberStringYesA unique merchant-side transaction ID used to link this payment request to an order in your system.
amountStringYesThe specific monetary value requested for the transaction (e.g., “11000.00”).
qr_dataStringYesThe raw EMVCo string. This is the actual data encoded inside the QR code image.
customer_nameStringYesThe full name of the customer associated with the payment account.
customer_emailStringNoThe registered email address used for sending digital receipts or notifications.
customer_phoneStringNoThe mobile number of the customer, usually formatted with the country code (e.g., 0821…).
customer_locationStringNoThe city or geographic region of the customer during the transaction.

Request Example

{
  "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"
}

Response

Response Structure

The response from the API will be in JSON format and will contain the following fields:

KeyData TypeMandatoryDescription
response_codeStringYesResponse code (see Response Code appendix)
response_messageStringYesResponse message (see Response Code appendix)
dataObjectNo
> transaction_idStringYesA unique identifier generated by system.
> reference_numberStringYesA unique identifier generated by merchant.
> transaction_statusObjectYesAn object containing the current state of the payment.
>> codeStringYesThe status code (e.g., “05”).
>> descStringYesThe human-readable status (e.g., “Canceled”, “Success”, or “Pending”).
> qr_dataStringYesThe raw EMVCo string. This is the actual data encoded inside the QR code image.
> typeStringYesThe QRIS model used. cpm, mpm, mpm-dynamic, mpm_static where the merchant shows a QR unique to that bill.
> scopeStringYesThe operational scope (e.g., “issuer”), indicating the entity processing the request.
> post_timestampStringYesThe Unix timestamp (in milliseconds) when the transaction was first created/posted.
> processed_timestampStringYesThe Unix timestamp (in milliseconds) when the transaction reached its final status.
> net_amountObjectNoThe Final Amount received by the merchant after deducting the fees.
>> valueStringYesNet amount for the transaction
>> currencyStringYesCurrency of the transaction
> feeObjectNoThe transaction fee or service charge deducted from the transaction.
>> valueStringYesFee amount for the transaction
>> currencyStringYesCurrency of the transaction
> gross_amountObjectNoThe Total Amount paid by the customer (Net + Fee).
>> valueStringYesGross amount for the transaction
>> currencyStringYesCurrency of the transaction

Response Example

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"
  }
}

How to Generate Request Signature

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.

Signature Algorithm

The signature uses HMAC-SHA512 algorithm with the following steps:

Step 1: Normalize the Request Body

Sort all JSON object keys recursively in alphabetical order, then encode using:

  • JSON_UNESCAPED_UNICODE - Don’t escape unicode characters
  • JSON_UNESCAPED_SLASHES - Don’t escape forward slashes

Example:

// 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"}

Step 2: Hash the Normalized Body

Hash the normalized JSON string using SHA-256:

hashedBody = SHA256(normalizedBody)

Example:

hashedBody = "a1b2c3d4e5f6789..."

Step 3: Build the String to Sign

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 2
  • TIMESTAMP = Unix timestamp in seconds (from X-Timestamp header)

Example:

POST:/api/v1.0/disbursements/abc123/transfer:eyJ0eXAiOiJKV1QiLCJhbGc...:a1b2c3d4e5f6789...:1714618220

Step 4: Generate HMAC-SHA512 Signature

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).

Code Examples

PHP Example

<?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'
);
?>

JavaScript/Node.js Example

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'
);

Python Example

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'
)

Important Notes

  1. Timestamp Format: Must be Unix timestamp in seconds (not milliseconds, not ISO 8601)
  2. Access Token: Extract from Authorization header without “Bearer ” prefix
  3. Endpoint: Must include the full path starting with /api/v1.0/... and include the {account_id} parameter
  4. JSON Normalization: Keys must be sorted recursively for nested objects
  5. Hash Output: Use hexadecimal encoding (not base64)
  6. Client Secret: Use your client_secret from merchant credentials, NOT your client_id or api_key

Security Validation

SingaPay will:

  1. Verify the X-PARTNER-ID matches an existing API key
  2. Retrieve the corresponding client_secret from the credentials table
  3. Regenerate the signature using the same algorithm
  4. Use constant-time comparison (hash_equals) to prevent timing attacks
  5. Reject the request if signatures don’t match