> ## Documentation Index
> Fetch the complete documentation index at: https://docs.singapay.id/llms.txt
> Use this file to discover all available pages before exploring further.

# QRIS Issuer Webhook

> Real-time notifications when a QRIS Issuer (money out) transaction completes or fails. Track success and failure outcomes for QRIS issuer payments via the shared disbursement_notif_url.

<Note>
  **Money Out Operation** — This webhook fires when a QRIS Issuer transaction is completed (success or failed), transferring funds from your SingaPay account to a merchant via QRIS.
</Note>

## Information

<div className="overflow-x-auto">
  | Method | Path | Format | Authentication |
  | - | - | - | - |
  | <span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-bold bg-blue-500 text-white">POST</span> | `https://your-webhook-url/callback` | json | HMAC SHA512 Signature |
</div>

SingaPay sends a `POST` request to your configured `disbursement_notif_url` when a QRIS Issuer transaction is completed (success or failed).

<Info>
  This webhook may share a callback URL with other event types. See [Shared webhook endpoints](/api-reference/webhooks/shared-endpoints) for routing by `event` value.
</Info>

***

## Request Details

### Headers

| Field | Value | Type | Mandatory | Description |
| - | - | - | :-: | - |
| `Content-Type` | `application/json` | Alphabetic | Yes | Specifies JSON format for the request body |
| `User-Agent` | `SingaPaymentGateway/1.0` | Alphabetic | Yes | Identifies the source of the webhook |
| `Accept` | `application/json` | Alphabetic | Yes | Expected response format |
| `X-PARTNER-ID` | — | Alphanumeric | Yes | Your API Key from the merchant dashboard |
| `X-Signature` | — | Alphanumeric | Conditional | HMAC SHA512 signature (128 chars). Included when signature security is enabled |
| `X-Timestamp` | — | Numeric | Conditional | Unix timestamp in **seconds**. Included when signature security is enabled |
| `Authorization` | `Bearer <access_token>` | Alphanumeric | Conditional | JWT bearer token; included when signature security is enabled |

<Note>
  Signature-related headers (`X-Signature`, `X-Timestamp`, `Authorization`) are included when your merchant account has API credentials configured and signature security is enabled. When enabled, extract the access token from the `Authorization` header and use it as-is in the string to sign. See [How to Validate Signature](#how-to-validate-signature) below.
</Note>

***

### Body Parameters

<ParamField body="response_code" type="string" required>
  Response code. See [Appendix 01 — Response Codes](#01-response-codes). Example: `SP000`
</ParamField>

<ParamField body="response_message" type="string" required>
  Human-readable response message. Example: `Successful`
</ParamField>

<ParamField body="event" type="string" required>
  Transaction type identifier. Always `"qris-issuer"` for this webhook. Use this to distinguish from `"disbursement"` and `"ewallet-topup"` on the shared URL.
</ParamField>

<ParamField body="data" type="object" required>
  Response payload object.

  <Expandable title="data fields">
    <ParamField body="transaction_id" type="string" required>
      System-generated transaction ID. Example: `"112220251111135424691"`
    </ParamField>

    <ParamField body="reference_number" type="string" required>
      Your unique merchant reference number. Example: `"123456789123"`
    </ParamField>

    <ParamField body="transaction_status" type="object" required>
      Transaction status object. See [Appendix 02 — Transaction Status](#02-transaction-status).

      <Expandable title="transaction_status fields">
        <ParamField body="code" type="string" required>
          Status code. E.g. `"00"` (Success), `"06"` (Failed)
        </ParamField>

        <ParamField body="desc" type="string" required>
          Status description. E.g. `"Success"`, `"Failed"`
        </ParamField>
      </Expandable>
    </ParamField>

    <ParamField body="post_timestamp" type="string" required>
      Transaction creation time in Unix **milliseconds**. Example: `"1762844064000"`
    </ParamField>

    <ParamField body="processed_timestamp" type="string">
      Processing completion time in Unix **milliseconds**. Empty string if the transaction failed.
    </ParamField>

    <ParamField body="qr_data" type="string" required>
      The raw EMVCo string encoded in the QR code image.
    </ParamField>

    <ParamField body="type" type="string" required>
      QR code type. E.g. `"mpm"`, `"cpm"`, `"mpm-dynamic"`, `"mpm-static"`
    </ParamField>

    <ParamField body="scope" type="string" required>
      QR code scope. E.g. `"issuer"`, `"acquirer"`
    </ParamField>

    <ParamField body="gross_amount" type="object" required>
      Total amount including fees.

      <Expandable title="gross_amount fields">
        <ParamField body="currency" type="string" required>ISO 4217 currency code. Example: `"IDR"`</ParamField>
        <ParamField body="value" type="string" required>Gross amount value. Example: `"21500.00"`</ParamField>
      </Expandable>
    </ParamField>

    <ParamField body="fee" type="object" required>
      Transfer fee details.

      <Expandable title="fee fields">
        <ParamField body="name" type="string">Fee description. Example: `"Transfer Fee"`</ParamField>
        <ParamField body="currency" type="string" required>ISO 4217 currency code.</ParamField>
        <ParamField body="value" type="string" required>Fee amount. Example: `"500.00"`</ParamField>
      </Expandable>
    </ParamField>

    <ParamField body="net_amount" type="object" required>
      Amount paid to the merchant (`gross_amount − fee`).

      <Expandable title="net_amount fields">
        <ParamField body="currency" type="string" required>ISO 4217 currency code.</ParamField>
        <ParamField body="value" type="string" required>Net amount value. Example: `"21000.00"`</ParamField>
      </Expandable>
    </ParamField>

    <ParamField body="balance_after" type="object" required>
      Your account balance after the transaction. Both fields are `null` for failed transactions.

      <Expandable title="balance_after fields">
        <ParamField body="currency" type="string">ISO 4217 currency code. `null` if failed.</ParamField>
        <ParamField body="value" type="string">Balance value. `null` if failed. Example: `"120000.00"`</ParamField>
      </Expandable>
    </ParamField>

    <ParamField body="rrn" type="string">
      Retrieval Reference Number issued by the QRIS switching network. Used for reconciliation with the acquirer/switcher. Empty string if the transaction failed. Example: `"9f3kd82p1a77"`
    </ParamField>

    <ParamField body="failed_code" type="string">
      Error code. Only present when the transaction failed. Example: `"CONNECTION_ERROR"`
    </ParamField>

    <ParamField body="failed_reason" type="string">
      Human-readable error description. Only present when the transaction failed. Example: `"Connection timeout to vendor"`
    </ParamField>
  </Expandable>
</ParamField>

***

### Payload Examples

<CodeGroup>
  ```json Success theme={null}
  {
    "response_code": "SP000",
    "response_message": "Successfully",
    "event": "qris-issuer",
    "data": {
      "transaction_id": "912042026071011453372901847",
      "reference_number": "INVQR-20260710-00931",
      "transaction_status": {
        "code": "00",
        "desc": "Success"
      },
      "post_timestamp": "1783650123000",
      "processed_timestamp": "1783650124512",
      "qr_data": "00020101021226620015ID.SINGAPAY.WWW0118936011122334450002021035224094080303UME51440014ID.CO.QRIS.WWW02153559174130998870303UME5204601153033605405150005802ID5903eos6005DEPOK6105746786221051017730486640703C0163044A21",
      "type": "mpm-dynamic",
      "scope": "issuer",
      "gross_amount": {
        "currency": "IDR",
        "value": "50750"
      },
      "fee": {
        "currency": "IDR",
        "value": "750"
      },
      "net_amount": {
        "currency": "IDR",
        "value": "50000"
      },
      "balance_after": {
        "currency": "IDR",
        "value": "184320450.00"
      },
      "rrn": "9f3kd82p1a77"
    }
  }
  ```

  ```json Failed theme={null}
  {
    "response_code": "SP001",
    "response_message": "Transaction Failure",
    "event": "qris-issuer",
    "data": {
      "transaction_id": "912042026071011453372901848",
      "reference_number": "INVQR-20260710-00932",
      "transaction_status": {
        "code": "06",
        "desc": "Failed"
      },
      "post_timestamp": "1783650321000",
      "processed_timestamp": "",
      "qr_data": "00020101021226620015ID.SINGAPAY.WWW0118936011122334450002021035224094080303UME51440014ID.CO.QRIS.WWW02153559174130998870303UME5204601153033605405150005802ID5903eos6005DEPOK6105746786221051017730486640703C0163044A21",
      "type": "mpm-dynamic",
      "scope": "issuer",
      "gross_amount": {
        "currency": "IDR",
        "value": "50750"
      },
      "fee": {
        "currency": "IDR",
        "value": "750"
      },
      "net_amount": {
        "currency": "IDR",
        "value": "50000"
      },
      "balance_after": {
        "currency": null,
        "value": null
      },
      "rrn": "",
      "failed_code": "CONNECTION_ERROR",
      "failed_reason": "Connection timeout to vendor"
    }
  }
  ```
</CodeGroup>

***

***

## Security and responses

Return HTTP `200` promptly after validating the request. For retry behavior, see [Webhook retry mechanism](/api-reference/webhooks/retry-mechanism).

Verify every webhook using [Security and signature validation](/api-reference/webhooks/security-and-signature). 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`).

***

## QRIS Issuer 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 payment.

```php Detecting Duplicate Webhooks theme={null}
$referenceNumber = $payload['data']['reference_number'];

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

processQrisIssuerPayment($payload);
```

### Timestamp Formats

<Warning>
  QRIS Issuer webhooks use **two different timestamp formats**. Don't mix them up.
</Warning>

| Field | Format | Example | Notes |
| - | - | - | - |
| `X-Timestamp` header | Unix seconds | `1695711945` | Used for signature validation |
| `post_timestamp` (body) | Unix milliseconds | `1762844064000` | Divide by 1000 to convert |
| `processed_timestamp` (body) | Unix milliseconds | `1762844065000` | Empty string if transaction failed |

When validating signatures, always use the **X-Timestamp header value** (in seconds), not the timestamps from the request body (which are in milliseconds).

```php Converting millisecond timestamps theme={null}
$postTimestamp = $payload['data']['post_timestamp']; // "1762844064000"
$date = new DateTime();
$date->setTimestamp((int)($postTimestamp / 1000));

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

### Balance Tracking

The `balance_after` field shows your account balance after the transaction:

* For **successful** transactions: remaining balance after deduction
* For **failed** transactions: both `currency` and `value` are `null` (no balance change)
* Use this to verify balance consistency with your system
* All amounts are in IDR (Indonesian Rupiah)

```php Balance Consistency Check theme={null}
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) {
        logBalanceDiscrepancy($transactionId, $expectedBalance, $balanceAfter);
    }
}
```

***

## Appendix

### 01: Response Codes

| Code | Message | Description | Action |
| - | - | - | - |
| `SP000` | Successfully | Success | Check inquiry status |
| `SP001` | Transaction Failure | Failed transaction | Check inquiry status |
| `SP002` | General Failure | Internal server error | Check inquiry status |
| `SP003` | Insufficient Balance | Account balance too low | Check inquiry status |
| `SP004` | Duplicate Reference Number | Reference already used | — |
| `SP005` | Timeout | Gateway timeout | Check inquiry status |
| `SP006` | Exceed Beneficiary Limit | Beneficiary amount limit reached | Check inquiry status |
| `SP007` | Exceed Account Limit | Account transaction limit reached | Contact IT Support |
| `SP008` | Invalid Reference Number | Reference does not exist | — |
| `SP009` | Transaction Not Found | Transaction not found | — |
| `SP010` | Beneficiary Account Not Found | Account not found | — |
| `SP011` | Beneficiary Vendor Not Active | Vendor not active | — |
| `SP012` | Bad Request | Bad request | — |
| `SP013` | Unauthorized | Unauthorized | — |
| `SP014` | Not Found | Not found | — |
| `SP015` | Forbidden | Forbidden | — |
| `SP016` | Signature Invalid | Signature invalid | — |
| `SP017` | Unauthorized IP | IP not authorized | Contact IT Support |
| `SP018` | Validation Error | Payload validation error | — |
| `SP019` | General Error | General error | Contact IT Support |
| `SP020` | Merchant Account Not Found | Merchant account not found | — |

### 02: Transaction Status

| Code | Status | Notes |
| :-: | - | - |
| `00` | Success | Transaction completed successfully |
| `01` | Initiated | Payment call not yet received; retry is possible |
| `02` | Paying | Pending/Suspect — retry status check in a few minutes; awaiting final callback |
| `03` | Pending | Pending/Suspect — retry status check in a few minutes; awaiting final callback |
| `04` | Refunded | Reversal transaction |
| `05` | Canceled | Create a new transaction |
| `06` | Failed | Transaction failed |
| `07` | Not Found | Create a new transaction |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.