Skip to main content
Money Out Operation — This webhook fires when funds are transferred from your SingaPay account to a beneficiary’s bank account.

Information

MethodPathFormatAuthentication
POSThttps://your-webhook-url/callbackjsonHMAC SHA512 Signature
SingaPay sends a POST request to your configured disbursement_notif_url when a disbursement transaction is successfully processed or fails.
This webhook may share a callback URL with other event types. See Shared webhook endpoints for routing by event value.

Request Details

Headers

FieldValueTypeMandatoryDescription
Content-Typeapplication/jsonAlphabeticYesSpecifies JSON format for the request body
User-AgentSingaPaymentGateway/1.0AlphabeticYesIdentifies the source of the webhook
Acceptapplication/jsonAlphabeticYesExpected response format
X-PARTNER-IDAlphanumericYesYour API Key from the merchant dashboard
X-SignatureAlphanumericYesHMAC SHA512 signature (128 chars) for request verification
X-TimestampNumericYesUnix timestamp in seconds when the request was sent
AuthorizationBearer <access_token>AlphanumericYesJWT bearer token; also used as component in the signature
All signature-related headers (X-Signature, X-Timestamp, Authorization) are always included when your merchant account has API credentials configured. Extract the access token from the Authorization header and use it as-is in the string to sign. See How to Validate Signature below.

Body Parameters

response_code
string
required
Response code. See Appendix 01 — Response Codes. Example: SP000
response_message
string
required
Human-readable response message. Example: Successfully
event
string
required
Transaction type identifier. Always "disbursement" for bank transfer. Use this to distinguish from "ewallet-topup" and "qris-issuer" on the shared URL.
data
object
required
Response payload object.

Payload Examples

{
    "response_code": "SP000",
    "response_message": "Successfully",
    "event": "disbursement",
    "data": {
        "transaction_id": "101222025122910292195055674",
        "reference_number": "11111111118",
        "transaction_status": {
            "code": "00",
            "desc": "Success"
        },
        "post_timestamp": "1766978961000",
        "processed_timestamp": "1766978962000",
        "bank": {
            "code": "002",
            "name": "BRI",
            "account_name": "Dummy Test Account Internal",
            "account_number": "11111111118"
        },
        "gross_amount": {
            "currency": "IDR",
            "value": "12504.00"
        },
        "fee": {
            "currency": "IDR",
            "value": "2500"
        },
        "net_amount": {
            "currency": "IDR",
            "value": "10004.00"
        },
        "balance_after": {
            "currency": "IDR",
            "value": "829988"
        },
        "notes": "test transfer"
    }
}


Security and responses

Return HTTP 200 promptly after validating the request. For retry behavior, see Webhook retry mechanism. Verify every webhook using Security and signature validation. Use your configured callback path when building StringToSign. Handle duplicate deliveries idempotently using stable identifiers from the payload (for example transaction_id or reff_no).

Disbursement-Specific Notes

Idempotency

Each reference_number can only be used once per account. If you retry with the same reference_number, you’ll receive the existing transaction status rather than creating a duplicate transfer.
Detecting Duplicate Webhooks
$referenceNumber = $payload['data']['reference_number'];

// Check if already processed
$existingTransaction = findTransactionByReference($referenceNumber);
if ($existingTransaction) {
    // Already processed — return 200 without re-processing
    http_response_code(200);
    exit;
}

processNewDisbursement($payload);

Timestamp Formats

Disbursement webhooks use two different timestamp formats. Don’t mix them up.
FieldFormatExampleNotes
X-Timestamp headerUnix seconds1695711945Used for signature validation
post_timestamp (body)Unix milliseconds1762339215000Divide by 1000 to convert
processed_timestamp (body)Unix milliseconds1762339215672Empty string if transaction failed
Converting millisecond timestamps
// Convert to DateTime
$postTimestamp = $payload['data']['post_timestamp']; // "1762339215000"
$date = new DateTime();
$date->setTimestamp((int)($postTimestamp / 1000));

// Or with Carbon (Laravel)
$date = \Carbon\Carbon::createFromTimestampMs($postTimestamp);

Balance Reconciliation

The balance_after field shows your account balance after each transaction. For failed transactions, both currency and value are null — no balance change occurred.
Balance Consistency Check
if ($payload['data']['transaction_status']['code'] === '00') {
    $balanceAfter = (float)$payload['data']['balance_after']['value'];
    $grossAmount  = (float)$payload['data']['gross_amount']['value'];

    $expectedBalance = $previousBalance - $grossAmount;
    if (abs($balanceAfter - $expectedBalance) > 0.01) {
        // Balance mismatch — log for investigation
        logBalanceDiscrepancy($transactionId, $expectedBalance, $balanceAfter);
    }
}

Appendix

01: Response Codes

CodeMessageDescriptionAction
SP000SuccessfullySuccessCheck inquiry status
SP001Transaction FailureFailed transactionCheck inquiry status
SP002General FailureInternal server errorCheck inquiry status
SP003Insufficient BalanceAccount balance too lowCheck inquiry status
SP004Duplicate Reference NumberReference already used
SP005TimeoutGateway timeoutCheck inquiry status
SP006Exceed Beneficiary LimitBeneficiary amount limit reachedCheck inquiry status
SP007Exceed Account LimitAccount transaction limit reachedContact IT Support
SP008Invalid Reference NumberReference does not exist
SP009Transaction Not FoundTransaction not found
SP010Beneficiary Account Not FoundAccount not found
SP011Beneficiary Vendor Not ActiveVendor not active
SP012Bad RequestBad request
SP013UnauthorizedUnauthorized
SP014Not FoundNot found
SP015ForbiddenForbidden
SP016Signature InvalidSignature invalid
SP017Unauthorized IPIP not authorizedContact IT Support
SP018Validation ErrorPayload validation error
SP019General ErrorGeneral errorContact IT Support
SP020Merchant Account Not FoundMerchant account not found

02: Transaction Status

CodeStatusNotes
00SuccessTransaction completed successfully
01InitiatedPayment call not yet received; retry is possible
02PayingPending/Suspect — retry status check in a few minutes; awaiting final callback
03PendingPending/Suspect — retry status check in a few minutes; awaiting final callback
04RefundedReversal transaction
05CanceledCreate a new transaction
06FailedTransaction failed
07Not FoundCreate a new transaction