Skip to main content
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:
1

Add Firebase SDK

Add Firebase dependencies to your build.gradle:
build.gradle (App)
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'
2

Add google-services.json

Download google-services.json from Firebase Console and place it in your app/ directory
3

Initialize Firebase

Initialize Firebase in your Application class:
NetPosApp.kt
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:
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:
email
string
Merchant email (with merchant_qr_ prefix for QR payments)
amount
number
Transaction amount in Naira
status
string
Transaction status: SUCCESS, FAILED, PENDING
customerName
string
Cardholder name
maskedPan
string
Masked card number
reference
string
Unique transaction reference
authCode
string
Authorization code from issuer
responseCode
string
ISO 8583 response code

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
number
Amount in kobo (divide by 100 for Naira)
payer_account_name
string
Sender’s account name
payer_account_number
string
Sender’s account number
details
string
Transfer details and narration
paid_at
string
Payment timestamp (ISO 8601)
reference
string
NIP transaction reference
session_code
string
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

SaveTransactionWorker.kt
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

TopicDescription
netpos_campaignMarketing and promotional messages
netpos_updatesApp updates and announcements
netpos_alertsCritical 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:
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:
1

Open Firebase Console

Navigate to Cloud Messaging section
2

Create Test Message

Click “Send your first message”
3

Configure Payload

Add custom data:
{
  "TransactionNotification": "{...}"
}
4

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

  • Verify google-services.json is correctly configured
  • Check FCM token registration with backend
  • Ensure device has internet connectivity
  • Check notification permissions (Android 13+)
  • 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

Build docs developers (and LLMs) love