> ## 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="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="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="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="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": "Successful",
    "event": "qris-issuer",
    "data": {
      "transaction_id": "112220251111135424691",
      "transaction_status": {
        "code": "00",
        "desc": "Success"
      },
      "qr_data": "00020101021226620015ID.SINGAPAY.WWW0118936012070412260002021035224094080303UME51440014ID.CO.QRIS.WWW02153559174130477690303UME5204601153033605405110005802ID5903eos6005DEPOK6105746786221051017730486640703C0163044D76",
      "type": "mpm-dynamic",
      "scope": "issuer",
      "reference_number": "123456789123",
      "post_timestamp": "1762844064000",
      "processed_timestamp": "1762844065000",
      "balance_after": {
        "value": "120000.00",
        "currency": "IDR"
      },
      "net_amount": {
        "value": "21000.00",
        "currency": "IDR"
      },
      "fee": {
        "value": "500.00",
        "currency": "IDR"
      },
      "gross_amount": {
        "value": "21500.00",
        "currency": "IDR"
      }
    }
  }
  ```

  ```json Failed theme={null}
  {
    "response_code": "SP001",
    "response_message": "Transaction Failure",
    "event": "qris-issuer",
    "data": {
      "transaction_id": "112220251111135424692",
      "transaction_status": {
        "code": "06",
        "desc": "Failed"
      },
      "qr_data": "00020101021226620015ID.SINGAPAY.WWW0118936012070412260002021035224094080303UME51440014ID.CO.QRIS.WWW02153559174130477690303UME5204601153033605405110005802ID5903eos6005DEPOK6105746786221051017730486640703C0163044D76",
      "type": "mpm-dynamic",
      "scope": "issuer",
      "reference_number": "123456789124",
      "post_timestamp": "1762844064000",
      "processed_timestamp": "",
      "balance_after": {
        "currency": null,
        "value": null
      },
      "net_amount": {
        "value": "21000.00",
        "currency": "IDR"
      },
      "fee": {
        "value": "500.00",
        "currency": "IDR"
      },
      "gross_amount": {
        "value": "21500.00",
        "currency": "IDR"
      },
      "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                                                       |
