QRIS Hook is a native Android Kotlin app that watches QRIS payment notifications from merchant apps and sends matching payment events to a webhook URL that you control.
QRIS Hook is provided for lawful notification monitoring and webhook automation where the user has the right and permission to access the relevant device, notifications, merchant account, and webhook endpoint.
You are solely responsible for how you configure, deploy, and use this application, including compliance with applicable laws, regulations, merchant terms, privacy obligations, and third-party service policies.
Any misuse of this application, including unauthorized access, privacy violations, data manipulation, fraudulent activity, or other unlawful use, is outside the control and responsibility of the author and contributors.
This software is provided "as is", without warranty of any kind. To the maximum extent permitted by law, the author and contributors are not liable for any claim, damage, loss, service interruption, data loss, or other liability arising from the use, misuse, inability to use, or distribution of this software.
- Notification monitoring with
NotificationListenerService. - Backend-agnostic webhook delivery using JSON
POSTrequests. - Optional
X-Webhook-Secretheader. - Built-in merchant selection through
MerchantRegistry. - Merchant-specific parsers for package matching and QRIS payment extraction.
- Webhook payloads with formatted payment data and raw notification fields.
- Local Room queue with background retry using WorkManager.
- Debug mode for capturing raw notification payloads from selected apps.
- Enable notification access for QRIS Hook in Android settings.
- Enter your webhook URL and optional secret in the app.
- Select the merchant apps you want QRIS Hook to monitor.
- When a matching QRIS payment notification arrives, QRIS Hook parses it into a payment event.
- The event is queued locally and delivered to your webhook in the background.
When debug mode is enabled, QRIS Hook stores raw notification payloads from selected apps instead of processing them as payment events. This is useful for adding or fixing merchant parsers.
QRIS Hook Active is the master switch for QRIS Hook notification handling. When it is turned off, QRIS Hook does not process incoming notifications at all, even if Debug Mode is enabled. Existing merchant selections, debug settings, and selected debug packages remain saved, but no payment events or debug payloads are created until QRIS Hook is active again.
When Debug Mode is enabled while QRIS Hook is active, debug capture takes priority over normal QRIS processing. Notifications from selected debug packages are saved as raw payloads, and webhook delivery is skipped for those notifications.
| QRIS Hook Active | Debug Mode | Behavior |
|---|---|---|
| Off | Off | Incoming notifications are ignored. |
| Off | On | Incoming notifications are still ignored; no debug logs are captured. |
| On | Off | Matching QRIS notifications are parsed and queued for webhook delivery. |
| On | On | Raw notifications from selected debug packages are saved; webhook delivery is skipped. |
QRIS Hook does not send webhook requests directly from Android's notification
listener callback. Matching QRIS notifications are first saved into the local
Room database, then QRIS Hook immediately attempts webhook delivery from the
app's background coroutine. WebhookDeliveryWorker is kept as the retry and
fallback path for failed or still-queued events.
Delivery flow:
NotificationProcessorreads the current settings.- If
QRIS Hook Activeis off, the notification is ignored. - If
Debug Modeis on, selected raw notification payloads are saved and webhook delivery is skipped. - In normal mode, the notification is matched against the selected merchant parsers.
- Every matching payment event is inserted into the local queue with status
Pending. - QRIS Hook immediately marks the event as
Sendingand sends it as a JSONPOSTrequest to the configured webhook URL. - A successful 2xx response marks the event as
Sent. - A failed request marks the event as
Failedand enqueuesWebhookDeliveryWorkerfor retry.
Pending does not mean the webhook request has already failed. It means the
event has been saved and is waiting to enter delivery. For newly received
notifications this should be brief because QRIS Hook attempts delivery
immediately after inserting the event. When delivery starts, the status is first
changed to Sending. If the request succeeds with a 2xx response, the status
becomes Sent. If the request fails, the status becomes Failed and the event
remains eligible for retry.
If a new event stays at Pending for more than a moment, immediate delivery did
not start for that event. Check that notification processing did not fail before
the delivery step and that the app has a saved webhook URL. For older queued
events, use the app's Retry pending button to enqueue
WebhookDeliveryWorker; the worker requires a connected network and may still be
affected by Android background scheduling.
Delivery status:
| Status | Meaning |
|---|---|
Pending |
Event has been queued and is waiting to enter delivery. |
Sending |
Webhook delivery is currently being attempted. |
Sent |
Webhook returned a successful 2xx response. |
Failed |
Last delivery attempt failed and the event remains eligible for retry. |
Retry behavior:
- New events are attempted immediately after they are saved.
- The worker processes retry/fallback events with status other than
Sent, oldest first, in batches of up to 10. - Successful 2xx responses mark the event as
Sent. - Non-2xx HTTP responses, network errors, invalid webhook URL errors, timeout errors, and Android network security errors are recorded as failures.
- Failed attempts update
lastError,lastResponseCode,lastResponseMessage,lastResponseBody,lastWebhookAttemptAtMillis, and incrementattempts. - If any event in the batch fails, the worker returns
Result.retry(). - WorkManager retries with exponential backoff starting at 30 seconds.
- New retry enqueues replace the existing unique retry work so they are not appended behind an old retry chain.
- QRIS Hook currently has no app-level maximum retry limit; failed events remain in the queue until they are eventually sent or the app data is cleared.
{
"event_id": "uuid",
"type": "qris.payment.success",
"merchant_id": "demo_merchant",
"source_package": "com.example.merchant",
"source_app": "Demo Merchant",
"received_at": "2030-05-21T12:00:00+07:00",
"notification": {
"source_package": "com.example.merchant",
"source_app": "Demo Merchant",
"title": "string",
"text": "string",
"big_text": "string",
"received_at": "2030-05-21T12:00:00Z"
},
"payment": {
"amount": 10000,
"currency": "IDR",
"sender_name": "John Doe",
"payment_source": "Demo"
},
"raw": {
"source_package": "com.example.merchant",
"source_app": "Demo Merchant",
"title": "string",
"text": "string",
"big_text": "string",
"received_at": "2026-05-21T12:00:00Z"
}
}If a secret is configured, the webhook request includes:
X-Webhook-Secret: your-secretRequirements:
- Android Studio or a configured Android SDK.
- JDK 17 or newer.
Build the debug APK:
./gradlew assembleDebugRun unit tests:
./gradlew testDebugUnitTestThe debug APK is generated at:
app/build/outputs/apk/debug/app-debug.apk
Pushes to main run the GitHub Actions release workflow. The workflow uses
Conventional Commits through semantic-release to choose the next version,
builds a signed release APK, creates a GitHub Release, and uploads the APK as a
release asset.
Commit types that create releases:
fix: ...creates a patch release, for example0.1.0to0.1.1.feat: ...creates a minor release, for example0.1.0to0.2.0.feat!: ...or a commit body withBREAKING CHANGE:creates a major release.
Required GitHub repository secrets:
ANDROID_KEYSTORE_BASE64ANDROID_KEYSTORE_PASSWORDANDROID_KEY_ALIASANDROID_KEY_PASSWORD
Create ANDROID_KEYSTORE_BASE64 from the release keystore:
base64 -w 0 release-keystore.jksThe current local fallback version is 0.1.0. To make semantic-release start
from that version, create and push the initial tag before the first automated
release:
git tag v0.1.0
git push origin v0.1.0Add a new parser that extends MerchantParser, then register it in MerchantRegistry.builtInParsers:
object ExampleMerchantParser : MerchantParser() {
override val merchantId = "merchant_id"
override val displayName = "Merchant Name"
override val merchantPackages = listOf("package.name.merchant")
private val successKeywords = listOf("qris", "success", "paid")
private val ignoreKeywords = listOf("failed", "refund", "promo")
override fun parse(notification: ObservedNotification): QrisPaymentEvent? {
// Implement merchant-specific parsing here.
return null
}
}If multiple selected parsers can handle the same notification, every valid event is added to the webhook queue.
Notification formats vary between merchant apps and app versions. If QRIS Hook does not recognize a merchant notification, you can help by sharing a debug payload.
- Enable notification access for QRIS Hook in Android settings.
- Open QRIS Hook and enable
Debug Mode. - Select the merchant app you want to capture.
- Trigger a real or test QRIS notification from that merchant app.
- Open
Debug Logsin QRIS Hook. - Open the captured log and tap
Copy. - Create a
Merchant parser requestGitHub issue with:- merchant app name
- package name
- expected payment details
- copied debug payload from QRIS Hook
Debug Mode
The copied debug payload is required. Parser requests without a debug payload cannot be implemented reliably.
Before posting the issue, redact sensitive data such as customer names, phone numbers, account identifiers, transaction IDs, or any other private information.
QRIS Hook is released under the MIT License.






