Skip to main content

Tap to Pay on Pocket

Initiate and accept contactless payments from your POS application via the SDK.

Intent sequence​

note

The code samples in this document use kotlin.

Click one of the intent endpoints in the sequence below to jump to the relevant section.

Setting up the SDK​

First, generate an API key in the Developer Portal by following this guide.

Then add the dependency to your project:

repositories {
mavenCentral()
}

dependencies {
implementation 'tech.dojo.pos:tap-to-pay-on-pocket:1.0.0'
}

Finally, initialise the SDK within your custom Application’s onCreate, using the API key from earlier and the software house ID provided to you by Dojo:

class ExamplePosApp : Application() {

override fun onCreate() {
super.onCreate()
initSdk()
}

private fun initSdk() {
DojoTapToPayOnPocket.initSdk(
context = this,
config = TapToPayOnPocket.Config(
apiKey = "[YOUR API KEY]",
softwareHouseId = "[YOUR SOFTWARE HOUSE ID]",
)
)
}
}

Customising the SDK​

TapToPayOnPocket.Config provides sensible defaults but allows you control the behaviour of the SDK should the need arise.

data class Config(
val apiKey: String,
val softwareHouseId: String,
val resellerId: String = softwareHouseId,
val apiEnvironment: DojoApiEnvironment = DojoApiEnvironment.PRODUCTION,
val appEnvironment: TapAppEnvironment = TapAppEnvironment.PRODUCTION,
val foregroundReconciliation: ForegroundReconciliationConfig = ForegroundReconciliationConfig(
val enabled: Boolean = true,
val windowMs: Long = 5_000,
val pollIntervalMs: Long = 1_000,
val networkConfig: ApiCallConfig = ApiCallConfig(
val connectTimeoutMs: Int = 10_000,
val readTimeoutMs: Int = 10_000,
val maxAttempts: Int = 3,
val firstRetryDelayMs: Long = 500,
val secondRetryDelayMs: Long = 1_000,
),
),
val processLifecycle: ProcessLifecycle = ProcessLifecycleOwnerAdapter(),
)

Reseller IDs​

resellerId can be used to identify the reseller who sells software on behalf of the EPOS company. This value will be unique and provided by Dojo to each reseller. In sandbox, use your assigned software-house-id and reseller-id if you have them; otherwise use the literal values softwareHouse1 and reseller1. Production integrations always use values assigned by Dojo.

Environments​

By default, the SDK targets the production environments. For development, you may be testing against a staging version of the TAP app. This version of TAP has a different package name. As the SDK uses explicit Intents, when initialising you can pass appEnvironment. appEnvironment controls the target Pocket environment. This should only be changed when developing internally against Dojo's staging environment.

apiEnvironment controls the Dojo API environment and should not be changed as PRODUCTION supports both sandbox and production API keys.

Foreground reconciliation​

By default, the SDK uses the Dojo API as a fallback when the POS app does not receive a response from the TAP app. This is triggered when focus is returned to the POS app without the SDK receiving a reply it expects. This can be disabled if you prefer to manually handle this. The timings can also be configured if the default values are not suitable for your app.

Process lifecycle observation​

The SDK uses Android Jetpack Startup library in order to listen to the process lifecycle. We recommend keeping this library enabled, however if you have disabled this then you must provide an alternative implemention through the processLifecycle property.

Multi-process​

We do not recommend using the SDK across multiple processes. By default, the SDK and its components will run in the default process. If you customise the manifest properties to run within a different process, the SDK will enforce all invocations to be within that same process. You must guard against trying to initialise the SDK or observing responses from alternative processes for correctness. Failure to comply will result in the SDK returning errors and the integration not working as expected.

R8/ProGuard​

If you are using R8, any shrinking or obfuscation rules required by this SDK will be included automatically.

Responses​

All responses from TAP are emitted via the DojoTapToPayOnPocket.responses Flow. Responses can be returned to the SDK whilst your app is in the background and therefore collection shouldn't directly make UI-modifications. We recommend subscribing to responses only once at the process-level to guarantee delivery. These can be persisted, observed or multicast to fit within your architecture.

For easier migrations from Intents to the SDK, though the Responses contain properties now, Responses also expose a Bundle. This is backwards compatible with the Intent/Bundles returned through Intent-integration.

If the SDK is expecting a response but does not receive one, will emit MissingResponse along with the request type and any known metadata, such as Payment Intent & Terminal Session IDs (when known), allowing you to reconcile.

Initialize​

Flow diagram: Tap Init Flow

graph TD
Start["val intent = Intent('com.dojo.action.INIT')"] --> Provisions[Device Provisions]

Provisions -->|Device has never been activated| Activation[Device Activation Flow<br/>Completed within Dojo Pay App]
Provisions --> Complete
Activation --> Complete

Complete[INIT_COMPLETE<br/>Check OnActivityResult]

Complete --> ResultFalse[INIT_RESULT false]
Complete --> ResultTrue[INIT_RESULT true]

style ResultFalse fill:#f96,stroke:#333
style ResultTrue fill:#3f3,stroke:#333
style Provisions fill:#d1f2eb,stroke:#333
style Complete fill:#b8c2f2,stroke:#333

Before defining any sale, refund, or split bill and gratuity logic, the first step is to initialize and provision TaP.

Request message​

Begin the initialization and provisioning process:

DojoTapToPayOnPocket.initDojo()

TaP provisioning may take up to two minutes to complete.

Prerequisites​

Should these checks fail, TaP will not be able to provision.

TaP Action​

Initialization is currently a UI-interactive process in TaP, requiring user input to enter the activation code and complete the device setup procedure. In future releases, this third party driven initialization will become a silent background process in TaP, with no UI interaction required. Any required setup data would be passed as a payload within the intent message.

Response​

TaP will return its response to DojoTapToPayOnPocket.responses as both properties and a Bundle within TapToPayOnPocket.Response.DojoInit:

  • An INIT_COMPLETE response is returned to indicate the status of the initialization.
  • The INIT_RESULT and INIT_RESULT_DETAIL extra fields reflect the initialization status.
    • INIT_RESULT returns a boolean value of true if it is successful and false if it is not.
    • The INIT_RESULT_DETAIL value will be either OK if initialization completes successfully or COMPLETED if it was already initialised before the call was made.
  • If there are any problems an error code will be provided as a string.
  • com.dojo.extra.TERMINAL_ID contains the unique identifier (known as a TID) of the current Pocket device. This is only returned from 26.07.15 onwards.

This message returns TaP initialization status information to the POS:

val pendingIntent = IntentCompat.getParcelableExtra(requestIntent, "com.dojo.extra.PENDING_INTENT", PendingIntent::class.java)

val response = Intent("com.dojo.action.INIT_COMPLETE").apply {
putExtra("com.dojo.extra.INIT_RESULT", isInitSuccess)
putExtra("com.dojo.extra.INIT_RESULT_DETAIL", "OK")
putExtra("com.dojo.extra.TERMINAL_ID", "58804222")
}

with (requireActivity()) {
pendingIntent?.send(this, Activity.RESULT_OK, response)
finish()
}

Required POS Action​

Within TapToPayOnPocket.Response.DojoInit:

The POS should check the isSuccess property to determine whether the TaP initialization succeeded or failed. When false, further information on the failure will be available in the detail property.

localPosClient.responses.collect { response ->
when (response) {
is Response.DojoInit -> {
val initResult = response.isSuccess
val initResultDetail = response.detail
}
}
}

Error codes include:

  • E1 - a payment session failed to start.
  • A10 - a provisioning error occurred.
  • T1 - a timeout caused the provisioning to be cancelled.
  • NO_TID - terminal ID not found after initialising. Recommended action: retry.

The POS app should respond to the messages using one of the following approaches:

  • No action required in the event of a successful device initialization.
  • Re-trying the initialization.
  • Notifying the user of a technical fault after multiple retries.

Sale​

Request message​

This message is to be used by the POS to request TaP to begin a contactless transaction.

scope.launch {
val result = DojoTapToPayOnPocket.sale(
amountInPennies,
reference = "Order #1234" // Optional
)
}

Prerequisites​

TaP must have been previously provisioned using INIT in order for this request to succeed.

TaP Action​

On receiving the request, TaP will begin a transaction, display the contactless acceptance screen, and wait for the presentation of a contactless EMV card.

Response​

The user should present a card to the device for contactless payment. TaP will then process the transaction, contact the acquiring bank for authorization, then show the result to the user.

Should the transaction fail, the reason will be displayed to the user (for example, card declined or card expired). These status codes will be returned to the POS in the response message.

This message returns transaction status and payment data to the POS.

Sender example (TaP):

val pendingIntent = IntentCompat.getParcelableExtra(requestIntent, "com.dojo.extra.PENDING_INTENT", PendingIntent::class.java)

val response = Intent("com.dojo.action.TX_COMPLETE").apply {
putExtra("com.dojo.extra.SALE_STATUS", "OK")
putExtra("com.dojo.extra.TRANSACTION_RESULT", "AUTHORISED") // enum: AUTHORISED, DECLINED, CANCELLED, FAILED
// ...
}

pendingIntent?.send(requireActivity(), Activity.RESULT_OK, response)

// finish() is called upon the user pressing the Done button

Expected POS Action​

TaP will return its response to DojoTapToPayOnPocket.responses as both properties and a Bundle within TapToPayOnPocket.Response.Sale.

The POS should check the transactionResult property to determine whether the transaction succeeded or failed. Additional properties exist for further details.

localPosClient.responses.collect { response ->
when (response) {
is Response.Sale -> {
val saleStatus = response.saleStatus
val transactionResult = response.transactionResult
val baseAmount = response.baseAmount
val gratuityAmount = response.gratuityAmount
val transactionId = response.transactionId
val authCode = response.authCode
val paymentMethod = response.paymentMethod
val cvm = response.cardholderVerificationMethod
val cardScheme = response.cardScheme
val cardApplicationLabel = response.cardApplicationLabel
val cardApplicationId = response.cardApplicationId
val cardPan = response.cardPan
val dateTime = response.dateTime
val paymentIntentId = response.paymentIntentId
val terminalSessionId = response.terminalSessionId
}
}
}
PropertyValueType
saleStatusNOT_PROVISIONED / OKString
baseAmountBase transaction amountInteger
gratuityAmountGratuity transaction amountInteger
transactionResultAUTHORISED/CANCELLED/DECLINED/FAILED/UNKNOWNEnum
authCodeTx Authorization CodeString
paymentMethodMANUAL_ENTRY/KEYED/SWIPE/CHIP/CONTACTLESS/UNKNOWNEnum
cardholderVerificationMethodPIN/SIGNATURE/DEVICE/NONE/UNKNOWNEnum
cardSchemeMastercard/Visa/Amex/DiscoverString
applicationLabelEMV application labelString
applicationIdEMV application IDString
cardPanCard Primary Account NumberString
dateTimeTime of the reversalString
transactionIdTaP transaction IDString
paymentIntentIdDojo API payment intent IDString
terminalSessionIdDojo API terminal session IDString

Refund​

Request message​

This message is to be used by the POS to request TaP to begin a contactless refund transaction.

scope.launch {
val result = DojoTapToPayOnPocket.refund(
amountInPennies,
)
}

Prerequisites​

TaP must have been previously provisioned using INIT in order for this request to succeed.

TaP Action​

On receiving the request, TaP will begin a transaction, display the contactless acceptance screen, and wait for the presentation of a contactless EMV card.

The user should present a card to the device for contactless payment. TaP will then process the refund transaction, contacting the acquiring bank for authorization and show the result to the user.

Response​

This message returns transaction status and payment data to the POS.

Should the transaction fail, the reason will be displayed to the user (for example, card declined or card expired). These status codes will be returned to the POS in the response message.

Sender example (TaP):

val pendingIntent = IntentCompat.getParcelableExtra(requestIntent, "com.dojo.extra.PENDING_INTENT", PendingIntent::class.java)

val response = Intent("com.dojo.action.TX_COMPLETE").apply {
putExtra("com.dojo.extra.SALE_STATUS", refundStatus)
putExtra("com.dojo.extra.TRANSACTION_RESULT", transactionResult)
// ...
}

pendingIntent?.send(requireActivity(), Activity.RESULT_OK, response)

// finish() is called upon the user pressing the Done button

Expected POS Action​

TaP will return its response to DojoTapToPayOnPocket.responses as a Bundle within TapToPayOnPocket.Response.Refund.

The POS should check the transactionResult property to determine whether the refund succeeded or failed. Additional properties exist for further details.

localPosClient.responses.collect { response ->
when (response) {
is Response.Refund -> {
val refundStatus = response.refundStatus
val transactionResult = response.transactionResult
val baseAmount = response.baseAmount
val transactionId = response.transactionId
val authCode = response.authCode
val paymentMethod = response.paymentMethod
val cvm = response.cardholderVerificationMethod
val cardScheme = response.cardScheme
val cardPan = response.cardPan
val dateTime = response.dateTime
val terminalSessionId = response.terminalSessionId
}
}
}
PropertyValueType
refundStatusOKString
baseAmountBase transaction amountInteger
transactionResultAUTHORISED/CANCELLED/DECLINED/FAILED/UNKNOWNEnum
authCodeTx Authorization CodeString
paymentMethodMANUAL_ENTRY/KEYED/SWIPE/CHIP/CONTACTLESS/UNKNOWNEnum
cardholderVerificationMethodPIN/SIGNATURE/DEVICE/NONE/UNKNOWNEnum
cardSchemeMastercard/Visa/Amex/DiscoverString
applicationLabelEMV application labelString
applicationIdEMV application IDString
cardPanCard Primary Account NumberString
dateTimeTime of the reversalString
transactionIdTaP transaction IDString
terminalSessionIdDojo API terminal session IDString

Last transaction details​

note

We recommend reconciling through reconcileSale and reconcileRefund. This functionality remains for parity with Intent-based integrations.

Request message​

This message is used by the POS to request details of the last executed transaction. It should be utilized in scenarios where the POS fails to receive a response from the preceding transaction. Example situations include:

  • The TaP app failed to send the response due to an internal failure.
  • The POS app failed during processing the response.
  • The device restarted before the POS could process the response.
val result = DojoTapToPayOnPocket.lastTx()

Prerequisites​

TaP must have been previously provisioned using INIT for this request to succeed.

TaP Action​

Upon receiving this message, TaP will return the available details from the last completed transaction, regardless of the transaction outcome (authorized, declined, or failed).

Response​

TaP will return its response to DojoTapToPayOnPocket.responses as a Bundle within TapToPayOnPocket.Response.LastTx.

Fields default to empty strings where data was not successfully retrieved, for example, where card data could not be fetched successfully. The payload contains the following data:

val pendingIntent = IntentCompat.getParcelableExtra(requestIntent, "com.dojo.extra.PENDING_INTENT", PendingIntent::class.java)

val response = Intent("com.dojo.action.LAST_TX_COMPLETE").apply {
putExtra("com.dojo.extra.SPLIT_BILL", isSplitBill)
// ...
}

with(requireActivity()) {
pendingIntent?.send(this, Activity.RESULT_OK, response)
finish()
}

Expected POS action​

The property splitBill indicates whether the last transaction was split between two or more customers. See the Split bills section for more info.

localPosClient.responses.collect { response ->
when (response) {
is Response.LastTx.Sale -> {
// Handle just like a Sale response
// e.g. val transactionResult = response.transactionResult
}
is Response.LastTx.Sale -> {
// Handle just like a SplitSale response
// e.g. val paymentsMade = response.paymentsMade
}
}
}

Split bills​

At the payment stage, the user can split a bill into constituent transactions and then recombine those into a single response.

A SplitSale response will be returned instead of Sale:

localPosClient.responses.collect { response ->
when (response) {
is Response.SplitSale -> {
val status = response.status
val paymentsMade = response.paymentsMade
val totalBaseAmount = response.totalBaseAmount
val totalPaidAmount = response.totalPaidAmount
val totalGratuityAmount = response.totalGratuityAmount
val splitTransactions = response.splitTransactions
}
}
}
PropertyValueType
paymentsMadeNumber of payments madeInteger
totalBaseAmountTotal amount to be paid in penniesInteger
totalPaidAmountTotal amount paid in penniesInteger
totalGratuityAmountTotal gratuity amount paid in penniesInteger
splitTransactionsList of payment splitsList(SplitTransaction)

The details of each transaction can be found in SplitTransaction.

localPosClient.responses.collect { response ->
when (response) {
is Response.SplitSale -> {
response.splitTransactions.forEach { tx->
val status = tx.status
val baseAmount = tx.baseAmount
val gratuityAmount = tx.gratuityAmount
val transactionResult = tx.transactionResult
val transactionId = tx.transactionId
val authCode = tx.authCode
val paymentMethod = tx.paymentMethod
val cvm = tx.cardholderVerificationMethod
val cardScheme = tx.cardScheme
val cardApplicationLabel = tx.cardApplicationLabel
val cardApplicationId = tx.cardApplicationId
val cardPan = tx.cardPan
val dateTime = tx.dateTime
}
}
}
}
PropertyValueType
txStatusOKString
baseAmountBase transaction amountInteger
gratuityAmountGratuity transaction amountInteger
transactionResultAUTHORISED/CANCELLED/DECLINED/FAILED/UNKNOWNEnum
authCodeTx Authorisation CodeString
paymentMethodMANUAL_ENTRY/KEYED/SWIPE/CHIP/CONTACTLESS/UNKNOWNEnum
cardholderVerificationMethodPIN/SIGNATURE/DEVICE/NONE/UNKNOWNEnum
cardSchemeMastercard/Visa/Amex/DiscoverString
cardApplicationLabelEMV application labelString
cardApplicationIdEMV application IDString
transactionIdTaP transaction IDString