Callback

PostmanTest on Postman

Parameters and structure of the callback notification.

Callback

When a buyer completes a payment, iPaymu will send a callback notification to your configured endpoint. This document covers the complete callback structure, required headers, and signature validation process.

Configuration

You can configure the callback content type in your iPaymu dashboard. Choose the format that works best for your system:

Content-TypeDescription
application/x-www-form-urlencodedDefault. Recommended for better stability and compatibility
application/jsonAlternative format if your system prefers JSON

Settings Links:

Secret Key for Signature Validation

To perform signature validation on callbacks, use your VA (Virtual Account) Number from your iPaymu account as the secret key.

How to get the Secret Key:

  1. Login to your iPaymu Dashboard
  2. Select Integration or Settings menu
  3. Click API Key
  4. Your VA Number will be displayed in the API Key or Merchant VA section

Example Secret Key: 1179001234567890

⚠️ Important: Never share your VA Number or store it in public repositories. Use environment variables to store the secret key in production.


Callback Flow Diagram

Here is the complete flow diagram of the callback process from iPaymu:

Diagram generated via mermaid

Important Notes:

  • If signature is invalid, don't process the transaction
  • Always respond with HTTP 200 to stop retries
  • Implement idempotency to avoid duplication

Important Notes

Before implementing callback handling, make sure you understand these key points:

TopicDescription
Retry MechanismiPaymu will automatically retry the callback if your server doesn't respond with 200 OK. Set up proper idempotency handling to avoid duplicate processing.
IdempotencySince retries may occur, always check transaction status in your database before processing. This prevents double-posting when the same callback arrives multiple times.
Signature ValidationAlways validate the X-Signature header to confirm the request genuinely comes from iPaymu. Never process callbacks without verification.
Secret KeyUse your Merchant VA Number as the secret key for signature validation.

Headers

Every callback request includes these headers. The X-Signature is crucial for security verification.

HeaderValueDescription
Acceptapplication/jsonThe content type iPaymu expects for responses
Content-Typeapplication/jsonFormat of the request body
X-External-ID20251111101052209120Unique identifier for this callback request
X-Signaturec9594cc8...HMAC-SHA256 signature for validation
X-Timestamp2025-11-11T10:10:52+07:00Timestamp when the callback was generated

Complete Payload Examples

Here are complete payload examples sent by iPaymu:

x-www-form-urlencoded Example

POST /callback HTTP/1.1
Host: your-domain.com
Content-Type: application/x-www-form-urlencoded
X-Signature: c9594cc8...
X-Timestamp: 2025-11-11T10:10:52+07:00

buyer_email=customer%40example.com&
buyer_name=John%20Doe&
buyer_phone=081234567890&
created_at=2025-11-11%2010%3A00%3A00&
expired_at=2025-11-11%2011%3A00%3A00&
fee=1500&
paid_at=2025-11-11%2010%3A10%3A52&
reference_id=ORDER123456&
sid=SESSION789&
status=berhasil&
status_code=1&
status_desc=Success&
channel=bca&
paid_off=98500&
product=Product%20Name&
quantity=1&
merchant=1179001234567890&
merchant_name=Your%20Store&
system_notes=Payment%20completed&
trscode=TRX789012&
trx_id=12345678&
unique_code=0&
via=va&
payment_no=1234567890&
va=1234567890&
url=https%3A%2F%2Fyour-domain.com%2Fcallback&
additional_info=%5B%5D&
transaction_status_code=1&
settlement_status=unsettle&
settlement_date=null&
expired_time=3600&
expired_unix=1762842000&
is_escrow=0&
is_refund=0&
is_sandbox=false&
total=100000&
amount=100000&
sub_total=100000&
referenceId=ORDER123456

application/json Example

{
  "buyer_email": "customer@example.com",
  "buyer_name": "John Doe",
  "buyer_phone": "081234567890",
  "created_at": "2025-11-11 10:00:00",
  "expired_at": "2025-11-11 11:00:00",
  "fee": "1500",
  "paid_at": "2025-11-11 10:10:52",
  "reference_id": "ORDER123456",
  "sid": "SESSION789",
  "status": "berhasil",
  "status_code": "1",
  "status_desc": "Success",
  "channel": "bca",
  "paid_off": "98500",
  "product": "Product Name",
  "quantity": "1",
  "merchant": "1179001234567890",
  "merchant_name": "Your Store",
  "system_notes": "Payment completed",
  "trscode": "TRX789012",
  "trx_id": "12345678",
  "unique_code": "0",
  "via": "va",
  "payment_no": "1234567890",
  "va": "1234567890",
  "url": "https://your-domain.com/callback",
  "additional_info": [],
  "transaction_status_code": "1",
  "settlement_status": "unsettle",
  "settlement_date": null,
  "expired_time": "3600",
  "expired_unix": "1762842000",
  "is_escrow": "0",
  "is_refund": "0",
  "is_sandbox": "false",
  "total": "100000",
  "amount": "100000",
  "sub_total": "100000",
  "referenceId": "ORDER123456"
}

Callback Data Structure

The following table shows all possible fields in a callback response. Pay close attention to the data types — using the wrong type during signature validation will cause hash mismatches.

Core Transaction Fields

FieldTypeDescriptionValidation Notes
trx_idIntegerUnique transaction ID in iPaymu systemConvert from String if receiving via Form Data
sidStringTransaction session ID-
reference_idStringYour merchant reference ID-
statusStringText status: berhasil, pending, expired-
status_codeIntegerStatus code: 1 (Success), 0 (Pending), -2 (Expired)Convert from String "1" to Integer 1

Amount Fields

FieldTypeDescriptionValidation Notes
sub_totalStringNet transaction amountLeave as String
totalStringGross transaction amountLeave as String
amountStringTotal transaction amountLeave as String
feeStringAdmin feeLeave as String
paid_offIntegerNet amount received (amount - fee)Convert from String to Integer

Timestamp Fields

FieldTypeDescriptionValidation Notes
created_atStringTransaction creation timeFormat: YYYY-MM-DD HH:MM:SS
expired_atStringTransaction expiration time-
paid_atStringPayment completion time-
settlement_statusStringSettlement statussettled or unsettle

Status Fields

FieldTypeDescriptionValidation Notes
transaction_status_codeIntegerOriginal status codeConvert from String to Integer
is_escrowBooleanEscrow indicatorConvert String "0" to false, "1" to true

Payment Details

FieldTypeDescriptionValidation Notes
viaStringPayment method (e.g., va)-
channelStringPayment channel (e.g., bca, mandiri)-
payment_noStringVA or payment numberImportant: Keep as String to preserve leading zeros
vaStringMerchant VA numberSame as payment_no. Only present for VA transactions
system_notesStringSystem-generated notes-

Buyer Information

FieldTypeDescriptionValidation Notes
buyer_nameStringBuyer name-
buyer_emailStringBuyer email-
buyer_phoneStringBuyer phone number-

Additional Fields

FieldTypeDescriptionValidation Notes
additional_infoArrayAdditional informationRequired: Include [] even if field is not sent
urlStringCallback URL-

Conditional Fields

Some fields behave differently depending on the payment method. Here's what to expect:

FieldBehavior
transaction_status_code1 = Payment settled immediately. 6 = Payment received (settlement may be processed separately)
payment_noPopulated for VA transactions. Empty for QRIS and e-wallet payments
vaOnly present for VA transactions. Not included for QRIS and e-wallet payments

Signature Validation

To ensure callbacks genuinely come from iPaymu, you must validate the signature on every request. Here's the step-by-step process:

Validation Steps

  1. Extract Data — Get all data from req.body
  2. Normalize Types — Convert values to their correct data types
    • String to Integer: trx_id, status_code, transaction_status_code, paid_off
    • String to Boolean: is_escrow
    • String [] to Array []: additional_info
  3. Handle Missing Fields — If additional_info is missing (common with x-www-form-urlencoded), add it manually as an empty array
  4. Sort Keys — Sort all keys in Ascending (A-Z) order with case sensitivity
  5. Convert to JSON — Serialize the sorted object to a JSON string
  6. URL Escape (Optional) — Escape forward slashes (/\/) if following strict PHP json_encode standards
  7. Generate Hash — Create HMAC-SHA256 using the JSON string and your secret key (VA number)
  8. Compare — Match your generated hash against the X-Signature header

Example Code Implementation

import crypto from 'crypto';
import express from 'express';

const app = express();

app.use(express.urlencoded({ extended: true }));

function normalizeData(rawData) {
    const result = {};
    for (const key in rawData) {
        let val = rawData[key];
        if (key === 'is_escrow') {
            result[key] = (val === 'true' || val === '1' || val === 1);
        }
        else if (['trx_id', 'status_code', 'transaction_status_code', 'paid_off'].includes(key)) {
            result[key] = parseInt(val, 10);
        }
        else if (key === 'additional_info') {
            if (val === '[]') result[key] = [];
            else result[key] = val;
        }
        else {
            result[key] = String(val);
        }
    }
    if (!result.hasOwnProperty('additional_info')) {
        result['additional_info'] = [];
    }
    return result;
}

function phpKsort(obj) {
    return Object.keys(obj)
        .sort((a, b) => a.localeCompare(b))
        .reduce((sortedObj, key) => {
            sortedObj[key] = obj[key];
            return sortedObj;
        }, {});
}

app.post('/callback', (req, res) => {
    const secretKey = '123456'; // Your iPaymu account VA Number (see Dashboard > Integration > API Key)
    const receivedSignature = req.headers['x-signature'];

    // Step 1 & 2: Get and normalize data
    const normalizedData = normalizeData(req.body);
    if (normalizedData.signature) delete normalizedData.signature;

    // Step 3 & 4: Sort keys
    const sortedData = phpKsort(normalizedData);
    
    // Step 5 & 6: Convert to JSON with slash escaping
    let jsonBody = JSON.stringify(sortedData);
    jsonBody = jsonBody.replace(/\//g, '\\/');

    // Step 7: Generate hash
    const calculatedSignature = crypto
        .createHmac('sha256', secretKey)
        .update(jsonBody)
        .digest('hex');

    // Step 8: Compare signatures
    if (calculatedSignature === receivedSignature) {
        console.log('✅ Signature valid');
        
        // TODO: Update transaction status in database
        // TODO: Ensure idempotency - check if already processed
        
        res.status(200).json({ status: 'OK' });
    } else {
        console.log('❌ Invalid signature');
        console.log('Expected:', receivedSignature);
        console.log('Got:     ', calculatedSignature);
        res.status(400).send('Invalid Signature');
    }
});

app.listen(PORT, () => {
  console.log('═══════════════════════════════════════════════════');
  console.log('  iPaymu Callback Handler - URL-encoded');
  console.log('═══════════════════════════════════════════════════');
  console.log(`  Server running on http://localhost:${PORT}`);
  console.log(`  Endpoint: POST http://localhost:${PORT}/callback`);
  console.log('═══════════════════════════════════════════════════');
});

Debugging Guide

If signature validation keeps failing, follow these debugging steps:

1. Log All Data

// Add detailed logging in your callback handler
app.post('/callback', (req, res) => {
    console.log('=== CALLBACK DEBUG ===');
    console.log('Headers:', JSON.stringify(req.headers, null, 2));
    console.log('Body (raw):', req.body);
    console.log('Content-Type:', req.headers['content-type']);
    console.log('X-Signature:', req.headers['x-signature']);
    
    // Continue validation...
});

2. Check JSON Output

// Print the JSON used for signature generation
const sortedData = phpKsort(normalizedData);
let jsonBody = JSON.stringify(sortedData);
jsonBody = jsonBody.replace(/\//g, '\\/');

console.log('JSON for signature:');
console.log(jsonBody);
// Output must exactly match what iPaymu expects

3. Compare Byte by Byte

// If signature doesn't match, compare character by character
function compareSignatures(sig1, sig2) {
    console.log('Signature comparison:');
    console.log('Expected:', sig1);
    console.log('Got:     ', sig2);
    
    for (let i = 0; i < Math.max(sig1.length, sig2.length); i++) {
        const c1 = sig1[i] || 'N/A';
        const c2 = sig2[i] || 'N/A';
        if (c1 !== c2) {
            console.log(`Mismatch at index ${i}: '${c1}' vs '${c2}'`);
        }
    }
}

Common Mistakes

Here are common mistakes when implementing callbacks:

1. Not Converting Data Types

Wrong:

const data = req.body; // Use directly without normalization

Correct:

const normalizedData = normalizeData(req.body);
// Ensure trx_id, status_code, etc. are Integer types

2. Incorrect Key Sorting

Wrong:

const sorted = Object.keys(data).sort(); // May be case-insensitive

Correct:

const sorted = Object.keys(data).sort((a, b) => a.localeCompare(b));

3. Not Escaping Slashes in JSON

Wrong:

const jsonBody = JSON.stringify(sortedData); // URL stays http://...

Correct:

let jsonBody = JSON.stringify(sortedData);
jsonBody = jsonBody.replace(/\//g, '\\/'); // URL becomes http:\/\/...

4. Missing additional_info

Wrong:

// Not adding additional_info if not in request

Correct:

if (!result.hasOwnProperty('additional_info')) {
    result['additional_info'] = [];
}

5. Using Wrong Secret Key

Wrong:

const secretKey = 'api_key_or_other'; // Not VA Number

Correct:

const secretKey = '123456'; // Your iPaymu account VA Number (see Dashboard > Integration > API Key)

Implementation Checklist

Before going live, make sure you have:

  • Configured callback URL in iPaymu dashboard
  • Using VA Number as secret key
  • Implemented correct data type normalization
  • Sorting keys in ascending order (A-Z)
  • Escaping forward slashes in JSON
  • Responding with HTTP 200 for valid requests
  • Implemented idempotency (duplicate check)
  • Logging all callbacks for debugging
  • Tested with dummy data from iPaymu
  • Have fallback mechanism if callback fails

Source: iPaymu Callback Documentation

On this page