Custom Apps

In A Nutshell
In a nutshell

You can build a custom android app to accept payment on the Paystack Terminal

Introduction

Paystack Terminal is powered by the Android operating system (OS) allowing developers to build Android apps on the Terminal. The Android OS enables apps to communicate with each other via Intents. With Intents, you can build an app that runs and accepts payments on Paystack Terminal.

Android API Level

The Terminal runs on Android 5.1, so you should set your minimum SDK version to level 22 (android:minSdkVersion="22"). Your target SDK can be anything greater than or equal to 22. You can learn more about this on the uses-sdk and API Levels documentation.

Building an intent

When implementing the payment flow for your custom app, you need to utilize an intent to communicate with the Paystack terminal app. An intent allows you to specify the particular app that should process your request. In this case, you’ll be specifying the Paystack terminal app to process payment for your app.

You can pass the following parameters when creating the intent for the Paystack terminal app:

ParameterDescriptionValue
Component nameThe package name of the Paystack Terminal appcom.paystack.pos
ActionA generic string capturing the operation to performIntent.ACTION_VIEW
ExtrasA key-value pair required to perform the desired operation. The keys will include the app's package name as a prefix. For example, com.paystack.pos.SETTINGSCheck the supported operations table below

Supported operations

These are the operations currently available on the terminal:

OperationExtra keyResult code
Fetch terminal detailscom.paystack.pos.PARAMETERS12
Initiate a transactioncom.paystack.pos.TRANSACT14
Open terminal settingscom.paystack.pos.SETTINGS-

Intent response

All supported operations, except the terminal settings, return a response with the following structure:

1data class PaystackIntentResponse (
2 val intentKey: String,
3 val intentResponseCode: Int,
4 val intentResponse: TerminalResponse
5)
6
ParameterDescription
intentKeyThis is the key passed into the putExtra method when initializing the intent. Possible values are listed in the Extra key column of the Supported Operations table
intentResponseCodeThis is the intent result code. Possible values are listed in the Result code column of the Supported Operations table
intentResponseThis is the response of the operation performed
1data class TerminalResponse(
2 val statusCode: String,
3 val message: String,
4 val data: String
5)
6
ParameterDescription
statusCodeWe use HTTP status codes to represent the status of the response
messageA short summary of the response
dataThis is a serialized object (JSON string) containing the result of your request. It can be deserialized into the respective object based on the operation being performed

Accept payment

To initiate a payment, you need to create an instance of the TransactionRequest object:

1data class TransactionRequest(
2 val amount: Int,
3 val offlineReference: String?,
4 val supplementaryReceiptData: SupplementaryReceiptData?,
5 val metadata: Map<String, Any>?
6)
7
ParameterRequired?Description
amountYesThe amount to charge the customer. The amount should be in kobo
offlineReferenceNoThis is a unique identifier for an invoice. You can set this value if you want to accept payment for a previously created invoice
supplementaryReceiptDataNoExtra details to add to the receipt on successful payment
metadataNoExtra data to append to the transaction
1data class SupplementaryReceiptData(
2 val developerSuppliedText: String?,
3 val developerSuppliedImageUrlPath: String?,
4 val barcodeOrQrcodeImageText: String?,
5 val textImageType: TextImageFormat?
6)
7
ParameterDescription
developerSuppliedTextAn extra text to add to the printed receipt
developerSuppliedImageUrlPathA publicly accessible URL for an image to be added to the receipt
barcodeOrQrcodeImageTextA text to be used to generate a barcode or QR code
textImageTypeSpecify the type of encoding for the image text
1enum class TextImageFormat {
2 QR_CODE,
3 AZTEC_BARCODE
4}
5

The instance of the TransactionRequest is then passed as an extra in your intent object:

1val gson = Gson()
2
3private fun makePayment() {
4 val transactionRequest = TransactionRequest(
5 amount = 2000,
6 offlineReference = null,
7 supplementaryReceiptData = null,
8 metadata = mapOf(
9 "custom_fields" to listOf(
10 CustomField(
11 display_name = "Extra Detail",
12 variable_name = "extra_detail",
13 value = "1234"
14 )
15 )
16 )
17 )
18
19 val transactionIntent = Intent(Intent.ACTION_VIEW).apply {
20 setPackage("com.paystack.pos")
21 putExtra("com.paystack.pos.TRANSACT", gson.toJson(transactionRequest))
22 }
23
24 // implementation below
25 startActivityForResult.launch(transactionIntent)
26}
27

Using the StartActivityForResult contract, the result can be parsed as follows:

1val TRANSACTION_RESULT_CODE = 14
2val TRANSACTION = "com.paystack.pos.TRANSACT"
3val startActivityForResult: ActivityResultLauncher<Intent> = registerForActivityResult(StartActivityForResult(), intentResultCallback())
4
5private fun intentResultCallback(): ActivityResultCallback<ActivityResult> {
6
7 return ActivityResultCallback { result: ActivityResult ->
8 val resultCode = result.resultCode
9 val intent = result.data
10 val paystackIntentResponse: PaystackIntentResponse
11 val terminalResponse: TerminalResponse
12
13 if (resultCode == TRANSACTION_RESULT_CODE) {
14 paystackIntentResponse = gson.fromJson(
15 intent?.getStringExtra(TRANSACTION),
16 PaystackIntentResponse::class.java
17 )
18 terminalResponse = paystackIntentResponse.intentResponse
19 val transactionResponse: TransactionResponse = gson.fromJson(
20 terminalResponse.data,
21 TransactionResponse::class.java
22 )
23
24 Toast.makeText(
25 applicationContext,
26 "Transaction ref: " + transactionResponse.reference,
27 Toast.LENGTH_SHORT
28 ).show()
29 }
30 else {
31 // handle invalid result code
32 }
33 }
34}

When the payment is completed, the response returned is an instance of the TransactionResponse object:

1import com.google.gson.annotations.SerializedName
2
3data class TransactionResponse(
4 val id: String?,
5 val amount: Int?,
6 val reference: String?,
7 val status: String?,
8 val currency: String?,
9 @SerializedName("country_code")
10 val countryCode: String?,
11 @SerializedName("paid_at")
12 val paidAt: String?,
13 val terminal: String?
14)

Fetch terminal details

Each terminal has a unique identifier and serial number attached to it. To fetch these details, you can construct an intent as follows:

1private fun fetchParameters(){
2 val parametersIntent = Intent(Intent.ACTION_VIEW).apply {
3 setPackage("com.paystack.pos")
4 putExtra("com.paystack.pos.PARAMETERS", "true")
5 }
6
7 // implementation below
8 startActivityForResult.launch(parametersIntent)
9}
10

Using the StartActivityForResult contract, the result can be parsed as follows:

1val PARAMETERS_RESULT_CODE = 12
2val PARAMETERS = "com.paystack.pos.PARAMETERS"
3val startActivityForResult: ActivityResultLauncher<Intent> = registerForActivityResult(StartActivityForResult(), intentResultCallback())
4
5private fun intentResultCallback(): ActivityResultCallback<ActivityResult> {
6
7 return ActivityResultCallback { result: ActivityResult ->
8 val resultCode = result.resultCode
9 val intent = result.data
10 val paystackIntentResponse: PaystackIntentResponse
11 val terminalResponse: TerminalResponse
12
13 if (resultCode == PARAMETERS_RESULT_CODE) {
14 paystackIntentResponse = gson.fromJson(
15 intent?.getStringExtra(PARAMETERS),
16 PaystackIntentResponse::class.java
17 )
18 terminalResponse = paystackIntentResponse.intentResponse
19 val parameters: ParameterResponse = gson.fromJson(
20 terminalResponse.data,
21 ParameterResponse::class.java
22 )
23 Toast.makeText(
24 applicationContext,
25 "Terminal ID: " + parameters.terminalId,
26 Toast.LENGTH_SHORT
27 ).show()
28
29 Toast.makeText(
30 applicationContext,
31 "Terminal Serial Number: " + parameters.serialNumber,
32 Toast.LENGTH_SHORT
33 ).show()
34 }
35 else {
36 // handle invalid result code
37 }
38 }
39}

The result is an instance of the PaystackIntentResponse class. Parsing the result gives access to the terminal details, which is an instance of the ParametersResponse class:

1import com.google.gson.annotations.SerializedName
2
3data class ParametersResponse(
4 @SerializedName("terminal_id")
5 val terminalId: String,
6 @SerializedName("serial_number")
7 val serialNumber: String
8)
9

Open terminal settings

The terminal settings activity allows you to perform administrative operations. You can programmatically open the settings page of the Terminal app from your app by passing the com.paystack.pos.SETTINGS extra in your Intent. This action doesn’t return a result so you’ll make use of the startActivity method to trigger the intent:

1private fun openSettings() {
2 val settingsIntent = Intent(Intent.ACTION_VIEW).apply {
3 setPackage("com.paystack.pos")
4 putExtra("com.paystack.pos.SETTINGS", "true")
5 }
6
7 startActivity(settingsIntent)
8}
9

Integration checklist

  • Ensure HTTPS is enabled on your app
  • The Android OS places a limit on the payload size that one app can send to another. You should check the limit and ensure you are not exceeding it.

Publishing your app

Upon the completion of the development and testing of your app, you would want to make it available on all your terminal device. We manage an app store that allows us to make your app available to your devices only. There are four steps to get your app deployed on all your devices:

  1. Generate your app APK
  2. Indicate interest in deploying a custom app via this form and we'll send you detailed guidelines for deployment
  3. Complete a preliminary scan following step 2 above
  4. Send us your APK

When we receive your submission, we’ll conduct a security review to ensure the app is safe for public use. Once the app is certified as safe, we’ll deploy your app to all your terminals. However, if the app doesn't pass the security review, we will share a document with feedback on remediation.