NetPOS uses Firebase Cloud Messaging (FCM) for delivering real-time webhook-style notifications to POS devices and backend systems.
Overview
Webhook notifications provide instant updates for:
- Transaction completions (card payments, QR payments)
- Virtual account transfers
- Payment status updates
- Campaign messages
- System alerts
Architecture
NetPOS webhook delivery uses Firebase Cloud Messaging:
Setup
Firebase Configuration
Add Firebase to your Android project:
Add Firebase SDK
Add Firebase dependencies to your build.gradle:dependencies {
// Firebase BOM
implementation platform('com.google.firebase:firebase-bom:30.4.1')
// Firebase Cloud Messaging
implementation 'com.google.firebase:firebase-messaging-ktx'
implementation 'com.google.firebase:firebase-analytics-ktx'
}
apply plugin: 'com.google.gms.google-services'
Add google-services.json
Download google-services.json from Firebase Console and place it in your app/ directory
Initialize Firebase
Initialize Firebase in your Application class:import com.google.firebase.FirebaseApp
import com.google.firebase.ktx.Firebase
import com.google.firebase.messaging.ktx.messaging
override fun onCreate() {
super.onCreate()
FirebaseApp.initializeApp(this)
// Subscribe to campaign notifications
Firebase.messaging.subscribeToTopic("netpos_campaign")
.addOnCompleteListener { task ->
if (task.isSuccessful) {
Timber.e("Subscribed to campaign topic")
}
}
}
Service Implementation
Create a Firebase Messaging Service:
MyFirebaseMessagingService.kt
package com.woleapp.netpos.services
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.core.app.NotificationCompat
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import com.google.gson.Gson
import com.woleapp.netpos.R
import com.woleapp.netpos.model.GetZenithPayByTransferUserTransactionsModel
import com.woleapp.netpos.model.PayWithCardNotificationModelResponse
import com.woleapp.netpos.ui.activities.MainActivity
import timber.log.Timber
class MyFirebaseMessagingService : FirebaseMessagingService() {
private val gson: Gson = Gson()
override fun onNewToken(token: String) {
Timber.e("New FCM token: $token")
sendRegistrationToServer(token)
super.onNewToken(token)
}
override fun onMessageReceived(remoteMessage: RemoteMessage) {
Timber.d("Message received from: ${remoteMessage.from}")
if (remoteMessage.data.isNotEmpty()) {
handleTransactionMessage(remoteMessage)
handleVirtualAccountMessage(remoteMessage)
}
}
private fun handleTransactionMessage(remoteMessage: RemoteMessage) {
remoteMessage.data["TransactionNotification"]?.let { notificationData ->
val transaction: PayWithCardNotificationModelResponse = gson.fromJson(
notificationData,
PayWithCardNotificationModelResponse::class.java
)
// Process transaction notification
if (transaction.email.contains(MERCHANT_QR_PREFIX)) {
createTransactionNotification(
getString(
R.string.notification_message_body,
transaction.amount.toDouble().formatCurrencyAmount(),
transaction.status,
transaction.customerName,
transaction.maskedPan
)
)
// Save to database
scheduleJobToSaveTransactionToDatabase(gson.toJson(transaction))
}
}
}
private fun handleVirtualAccountMessage(remoteMessage: RemoteMessage) {
remoteMessage.data["VirtualNotification"]?.let { notificationData ->
val transaction: GetZenithPayByTransferUserTransactionsModel = gson.fromJson(
notificationData,
GetZenithPayByTransferUserTransactionsModel::class.java
)
val transactionAmount = transaction.amount?.div(100) ?: 0.0
sendNotification(
"${transactionAmount.formatCurrencyAmount()} Received\n" +
"From: ${transaction.payer_account_name} (${transaction.details})"
)
// Save to database
scheduleJobToSaveTransactionToDatabase(gson.toJson(transaction))
}
}
private fun sendRegistrationToServer(token: String) {
// Register token with backend for targeted notifications
scheduleJobToRegisterNewToken(token)
}
}
Register Service
Add the service to AndroidManifest.xml:
<service
android:name=".services.MyFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
Webhook Payloads
Transaction Notification
Received when a card payment completes:
{
"TransactionNotification": "{
\"email\": \"[email protected]\",
\"amount\": 5000.00,
\"status\": \"SUCCESS\",
\"customerName\": \"JOHN DOE\",
\"maskedPan\": \"506066******1234\",
\"reference\": \"TXN202112011234\",
\"authCode\": \"123456\",
\"responseCode\": \"00\",
\"timestamp\": 1638360000000
}"
}
Payload Fields:
Merchant email (with merchant_qr_ prefix for QR payments)
Transaction amount in Naira
Transaction status: SUCCESS, FAILED, PENDING
Unique transaction reference
Authorization code from issuer
Virtual Account Notification
Received when a bank transfer is received:
{
"VirtualNotification": "{
\"amount\": 500000,
\"payer_account_name\": \"JOHN DOE\",
\"payer_account_number\": \"1234567890\",
\"details\": \"Transfer/0012345678\",
\"paid_at\": \"2021-12-01T10:30:00Z\",
\"reference\": \"NIP202112011234567\",
\"session_code\": \"123456\"
}"
}
Payload Fields:
Amount in kobo (divide by 100 for Naira)
Transfer details and narration
Payment timestamp (ISO 8601)
NIP transaction reference
Session code used for transfer (if applicable)
Notification Display
Create Notification
private fun sendNotification(messageBody: String) {
val intent = Intent(this, MainActivity::class.java)
intent.action = STRING_FIREBASE_INTENT_ACTION
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
intent.putExtra(TAG_NOTIFICATION_RECEIVED_FROM_BACKEND, true)
val pendingIntent = PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_IMMUTABLE
)
val channelId = "fcm_default_channel"
val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
val notificationBuilder = NotificationCompat.Builder(this, channelId)
.setContentTitle(getString(R.string.transacion_received))
.setStyle(NotificationCompat.BigTextStyle().bigText(messageBody))
.setSmallIcon(R.drawable.ic_netpos_logo)
.setContentText(messageBody)
.setAutoCancel(true)
.setSound(defaultSoundUri)
.setContentIntent(pendingIntent)
.setPriority(NotificationCompat.PRIORITY_HIGH)
val notificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
// Create notification channel for Android O+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
channelId,
getString(R.string.transacion_received),
NotificationManager.IMPORTANCE_HIGH
)
notificationManager.createNotificationChannel(channel)
}
notificationManager.notify(0, notificationBuilder.build())
}
Background Processing
Use WorkManager for reliable background processing:
private fun scheduleJobToSaveTransactionToDatabase(
transactionJson: String
) {
val inputData: Data = Data.Builder()
.putString(WORKER_INPUT_PBT_TRANSACTION_TAG, transactionJson)
.build()
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val work = OneTimeWorkRequest.Builder(
SaveTransactionFromFirebaseMessagingServiceToDbWorker::class.java
)
.setConstraints(constraints)
.setInputData(inputData)
.build()
WorkManager.getInstance(this)
.beginWith(work)
.enqueue()
}
Worker Implementation
class SaveTransactionFromFirebaseMessagingServiceToDbWorker(
context: Context,
params: WorkerParameters
) : Worker(context, params) {
override fun doWork(): Result {
val transactionJson = inputData.getString(WORKER_INPUT_PBT_TRANSACTION_TAG)
?: return Result.failure()
return try {
val transaction = gson.fromJson(
transactionJson,
TransactionResponse::class.java
)
// Save to database
val database = AppDatabase.getDatabaseInstance(applicationContext)
database.transactionDao().insertTransaction(transaction)
Result.success()
} catch (e: Exception) {
Timber.e(e, "Failed to save transaction")
Result.retry()
}
}
}
Topic Subscriptions
Subscribe to FCM topics for broadcast messages:
import com.google.firebase.ktx.Firebase
import com.google.firebase.messaging.ktx.messaging
// Subscribe to campaign notifications
Firebase.messaging.subscribeToTopic("netpos_campaign")
.addOnCompleteListener { task ->
if (task.isSuccessful) {
Timber.d("Subscribed to campaign topic")
Prefs.putBoolean("notification_campaign", true)
} else {
Timber.e("Subscription failed")
}
}
// Unsubscribe from topic
Firebase.messaging.unsubscribeFromTopic("netpos_campaign")
.addOnCompleteListener { task ->
if (task.isSuccessful) {
Timber.d("Unsubscribed from campaign topic")
}
}
Available Topics
| Topic | Description |
|---|
netpos_campaign | Marketing and promotional messages |
netpos_updates | App updates and announcements |
netpos_alerts | Critical system alerts |
Token Management
Register Device Token
Send FCM token to backend for targeted notifications:
private fun scheduleJobToRegisterNewToken(newToken: String) {
val inputData = Data.Builder()
.putString(WORKER_INPUT_FIREBASE_DEVICE_TOKEN_TAG, newToken)
.build()
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val work = OneTimeWorkRequest.Builder(
RegisterDeviceTokenToBackendOnTokenChangeWorker::class.java
)
.setConstraints(constraints)
.setInputData(inputData)
.build()
WorkManager.getInstance(this)
.beginWith(work)
.enqueue()
}
Token Refresh
Handle token refresh events:
override fun onNewToken(token: String) {
Timber.e("New FCM token: $token")
// Save locally
Prefs.putString(PREF_FCM_TOKEN, token)
// Register with backend
sendRegistrationToServer(token)
super.onNewToken(token)
}
Permissions
Required permissions in AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" />
<!-- For Android 13+ notification permissions -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
Runtime Permission (Android 13+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
NOTIFICATION_PERMISSION_REQUEST_CODE
)
}
}
Testing Webhooks
Send Test Notification
Use Firebase Console to send test notifications:
Open Firebase Console
Navigate to Cloud Messaging section
Create Test Message
Click “Send your first message”
Configure Payload
Add custom data:{
"TransactionNotification": "{...}"
}
Send to Device
Use your FCM token or topic name
Debug Logging
Enable FCM debug logging:
adb shell setprop log.tag.FCM DEBUG
adb logcat -s FCM
Best Practices
Idempotency
Use transaction references to prevent duplicate processing
Background Processing
Use WorkManager for reliable background transaction saving
Error Handling
Implement retry logic for failed webhook processing
Data Validation
Validate all webhook payloads before processing
Troubleshooting
Notifications not received
- Verify
google-services.json is correctly configured
- Check FCM token registration with backend
- Ensure device has internet connectivity
- Check notification permissions (Android 13+)
Notifications received but not displayed
- Verify notification channel creation for Android O+
- Check notification priority settings
- Ensure app is not in battery optimization
- Implement
onNewToken() callback
- Register token with backend on each refresh
- Handle token refresh during app updates
Next Steps
MQTT Integration
Combine with MQTT for comprehensive real-time notifications
API Endpoints
Use REST APIs for transaction queries and verification