Payment Application IntegrationPayment Application Integration
Table of Contents
Table of Contents
  • Table of Contents

Table of Contents

  • Introduction
  • Terms and Definitions
  • TerminalApp Configuration
  • Source Code Examples
  • Communication Protocol
    • Data flow
    • Framework
    • Data structures
  • Functions
    • Sale
    • Sale Auth
    • Capture
    • Void/Refund
    • Find
    • Close Batch
    • Transaction Reports
  • Integration Modes
    • Managed Mode
    • Proxy Mode
  • Broadcast Receiver Integration
    • Transaction Responses
    • Status Notifications
    • Comparison of Integration Modes
  • Cross-cutting Concerns
    • API Authentication
    • Transaction Identification
    • Duplicate Transaction Handling

Introduction

This document outlines integration strategies for Tidypay's payment application with POS systems. It focuses on the communication protocol, primary modes of integration: Managed Mode, Proxy Mode, and communication using BroadcastReceiver.

Terms and Definitions

TermDescription
Terminal AppPayment application provided by Tidypay
POS Client DemoPOS demo application supplied by Tidypay meant to host an example of the integration with Terminal App in different modes of operation.
POSPoint-Of-Sale system of the integrator
ProxyAppLightweight application that gets installed side-side with Terminal App to facilitate data exchange between the remote POS system and the Terminal App.
TMSTerminal Management System

TerminalApp Configuration

To ensure proper functionality of the Terminal App, the network and firewall settings shall be configured according to the following requirements:

  1. Internet Connectivity Checks:

    a. The Terminal App verifies internet availability by pinging the following addresses:

    1. http://google.com
    2. 8.8.8.8
  2. TMS Requests:

    a. Host: TMS host

    b. Port: 443

  3. Transaction Processing:

    a. Host: GatewayHost

    b. Port: 443

Source Code Examples

Enter image alt description

POS Client demo Android application that interacts with Terminal App in different modes.

In Tools folder there is the Proxy app.zip that contains

  • A simple demo proxy application.

  • Java PC program that can be used to test connections from a PC to a payment device.

Communication protocol

Data flow

Enter image alt description

Framework

The typical communication flow with using Android Intents to be executed between the integrator’s POS system and the Terminal App incurs the following phases:

  1. Registration of Intent for handling API requests.

Enable querying of installed applications on the device by either enabling All or selective through package names.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <queries>
        <package android:name="com.tidypos.terminal" />
        <package android:name="com.tidypay.softpos" />
    </queries>
    ...
    <!-- or -->
    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
        tools:ignore="QueryAllPackagesPermission" />
    ...
</manifest>
  1. Execute payment function

Start Terminal App by passing the command to execute via the Intent. To do this, the activity with the method

 startActivityForResult(intent, REQUEST_CODE);

shall be used. The REQUEST_CODE is a user-defined integer code.The response will be received in the onActivityResult method of the current activity, the prototype of which looks as follows:

onActivityResult(int requestCode, int resultCode, @Nullable Intent data)
  1. Reading information from the data intent on the POS side.

The accepted intent has a mandatory extras object named response containing a Map<String, String>. The data can be obtained as follows

Map<String, String> responseMap = (Map<String, String>) data.getSerializableExtra("response");

If the dataType key is present in the map and its string value is equal to 'list', then the intent should have a second extras named 'data' containing data on the previously sent request. The data itself is a Uri type containing a link to the file with the processing result.

The data can be obtained as follows:

Uri receivedUri = intent.getParcelableExtra("data");

In case of the presence of a second extras object, the map also contains the ’dataChecksum’' field, which contains the checksum of the file calculated by the SHA1 algorithm.

  1. Loading the generated file from a response

The generated file with the result is loaded through an 'InputStream' object, which is obtained using the getContentResolver().openInputStream(receivedUri) method. Previously received shall be passed as an input param.

The full example of the process that involves handling a list of data populated is presented in the Transaction reports.

The back channel data flow with real-time notifications and updates about payment progress from Terminal App to POS is described in BroadcastReceiver Integration.

Data structures

Regardless of the integration mode, the data fields used to populate requests and responses are specified in Tidypay Terminal API depending on the request being made.

Note! Even though required in the API specification, accountId and transactionId fields can be skipped from transmission in POS because Terminal App is capable of figuring them out based on the configuration applied. This applies to all methods with a few exceptions to be highlighted individually.

All API requests, regardless of a type, include a response data structure. However, the requests with an attachment, which deal with larger data volumes, also include a data structure. This data structure contains a URI linking to an attachment and is included in addition to the usual response structure.

The data structure in the response provides the URI to this temporary file containing the data. This data structure is sent alongside the response structure in the API response.

Besides the standard fields described in the specification, the terminal adds two additional fields in the response structure:

● dataType: indicates the format of the response (details below).

● dataChecksum: a checksum that can be used to validate the integrity of the attachment, if the last is included in the data structure.

The dataType field indicates how to interpret the response:

● 'no data'- there is nothing to analyze, the request returns no information.

● 'error'- the response generates an error. Details can be found in the responseCode and responseMessage fields.

● 'detail' (default type) - the response should be interpreted according to the format described in theTidypay Terminal API API documentation.

● 'list'- the response includes an attachment with the list of data in JSON format, and the data should be handled accordingly. This data type is used in transaction reporting functions.

Functions

Sale

API format

Sale

Example

Please refer to Example Of Sale in Managed mode in the managed mode and Example of Sale in Proxy mode

Sale Auth

API format

Sale Auth

Example

// Analogous to a Sale command example except request type being different as a line below
params.put(TerminalReq.RequestType, RequestType.SaleAuth);

Capture

API format

Capture

Example

private void processCapture() {
    MainApp app = (MainApp) getApplicationContext();

    TransactionResponse latest = app.getLatestTransactionResponse();

    if (latest != null && latest.getResponseType().equals(RequestType.SaleAuth.getValue())) {
        TerminalAPI.setCredentials(API_TIDYPAY_CREDENTIALS);
        Map<FieldName, Object> params = new HashMap<>();
        params.put(TerminalReq.RequestType, RequestType.Capture);
        params.put(TerminalReq.TransactionId, latest.getTransactionId());
        TerminalAPI.exec(this, params, TerminalAPI.CAPTURE_REQUEST_CODE);
    } else {
        ...
    }
}

Note! The TransactionId field is mandatory for the Capture function. Typically, POS is going to cache the response of the Sale-Auth transaction and use it whenever Capture is invoked..

Void/Refund

API format

Void and Refund.

Example

private void processRefundRequest(String transactionId) {
    TerminalAPI.setCredentials(API_TIDYPAY_CREDENTIALS);
    
    Map<FieldName, Object> params = new HashMap<>();
    params.put(TerminalReq.RequestType, RequestType.Refund);
    params.put(TerminalReq.TransactionId, transactionId);
    
    TerminalAPI.exec(this, params, TerminalAPI.REQUEST_CODE);
}

Note! The TransactionId field is mandatory and Transaction Code is optional.

Note! No matter what exactly is put into the Request Type field, Terminal App works out whether the Void or Refund gets applied depending on the transaction status, e.i, if it was settled already or not.

Voiding Logic

Voiding logic is designed so that if the terminal is uncertain about the status of a previous transaction then it will send a Void request for this transaction to the Gateway. In the gateway the status might indeed be approved but this has not reached the terminal. The voiding logic does not kick in before another action is taken in the Terminal App itself like performing a transaction request, running Get parameters, or something else that validates the connection to the Gateway.

Find requests can be used in the background of the POS system to fire off the validation to the Gateway prematurely without waiting for a subsequent transaction to occur.

Please refer to the Find section for more information about the *Find *function.

Find

Find request allows to fetch the status of the existing transaction - both settled and unsettled.

API format

Find

Example

private void processFindRequest(String transactionCode) {
    TerminalAPI.setCredentials(API_TIDYPAY_CREDENTIALS);
    
    Map<FieldName, Object> params = new HashMap<>();
    params.put(TerminalReq.RequestType, RequestType.Find);
    params.put(TerminalReq.TransactionCode, transactionCode);
    
    TerminalAPI.exec(this, params, TerminalAPI.FIND_REQUEST_CODE);
}

Note! Find response data contains less fields when hitting unsuccessful transaction as some fields are not fully transmitted to POS. This issue will be circumvented in one of the next Terminal App releases.

Close batch

API format

Close cycle

Example

private void processCloseCycleRequest() {
    TerminalAPI.setCredentials(API_TIDYPAY_CREDENTIALS);
    Map<FieldName, Object> params = new HashMap<>();
    params.put(TerminalReq.RequestType, RequestType.CloseCycle);
    TerminalAPI.exec(this, params, TerminalAPI.REQUEST_CODE);
}

Transaction reports

API format

Transaction List and Transaction Summary of the day

Example

The Transaction List API data flow and extraction can be viewed in the following pseudo code snippet. It implements the scenario that:

  • Searches for all the 'Unsettled' transactions made on the given terminal within the last 30 minutes.

  • EST time zone is facilitated. This time zone is set by default in the Sandbox environment.

  • The received 'list' of transactions gets downloaded in JSON format.

  • Checksum calculations, exception handling, and processing the results of the reports are omitted for the sake of simplicity.

private void processTransactionReportRequest() {
    TerminalAPI.setCredentials(API_TIDYPAY_CREDENTIALS);
    Map<FieldName, Object> params = new HashMap<>();
    params.put(TerminalReq.RequestType, RequestType.TransactionList);
    params.put(TerminalReq.SettlementStatus, "unsettled");
    params.put(TerminalReq.ReportBasis, "authorizationDate");
    params.put(TerminalReq.AccountId, 6001);
    params.put(TerminalReq.TerminalId, 3);

    // Get the current time in UTC
    Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
    SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMddHHmmss");
    // Change the time zone to EST. This is for Sandbox environment only
    formatter.setTimeZone(TimeZone.getTimeZone("EST"));
    String toTime = formatter.format(calendar.getTime());
    // Subtract 30 minutes from the current time
    calendar.add(Calendar.MINUTE, -30);
    String fromTime = formatter.format(calendar.getTime());
    params.put(TerminalReq.ReportFromDate, fromTime);
    params.put(TerminalReq.ReportToDate, toTime);

    TerminalAPI.exec(this, params, TerminalAPI.REPORT_REQUEST_CODE);
}

private void processTransactionReportResponse(Integer resultCode, Intent intent) {
    Map<FieldName, String> responseMap = TerminalAPI.getResponseParameters(intent);
    String checkSum = responseMap.get(TerminalRes.DataChecksumType);
    int countOfTransactions = 0;
    if ("list".equalsIgnoreCase(responseMap.get(TerminalRes.DataType))) {
        Uri receivedUri = intent.getParcelableExtra("data");
        if (receivedUri != null) {
            try (InputStream inputStream = getContentResolver().openInputStream(receivedUri);
                 BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
                StringBuilder stringBuilder = new StringBuilder();
                String line;
                while ((line = reader.readLine()) != null) {
                    stringBuilder.append(line);
                }
                String jsonString = stringBuilder.toString();
                // TODO: Check the integrity by calculating the SHA1 checksum of the received file against the value transmitted in the main 'response'
                try {
                    JSONArray jsonArray = new JSONArray(jsonString);
                    countOfTransactions = jsonArray.length();
                    getContentResolver().delete(receivedUri, null, null);
                } catch (JSONException e) {
                    // Handle JSON parsing error
                }
            } catch (Exception e) {
                // Handle Parsing error
            }
        }
    }
    // ...
}

Note! As opposed to other requests, among the mandatory input arguments, ‘accountId’ and ‘terminalId’ are present that accurately match the configured Terminal App on the given device. Without correctly specifying those, Terminal App will reject the request with the error message similar to this ‘User name or password are incorrect.’ A very common scenario for obtaining the required fields would be to extract them from a Sale transaction responses and use them eventually in Transaction report queries.

Integration Modes

Managed Mode (App-to-App Switching)

Overview

Managed mode implies installing a POS application on the terminal where the Terminal App resides. The illustration below shows how the interactions between POS and Tidypay components take place in this mode:

Enter image alt description

When POS attempts to make a payment transaction, the Intent with transaction details such as Amount, Transaction Code, etc. is sent to the Terminal App. Terminal App becomes a foreground process that completes the transaction before returning back a control to POS.

Apparently, the POS application runs in the background while payments and other operations get processed.

Features:

  • High-speed transaction processing with excellent stability.

  • Commonly used in mobile POS setups or all-in-one solutions.

  • Operations supported include: Sale, Refund, Void, Close Cycle, Find and others.

Technical Considerations

Managed Mode relies on Android's Intent mechanism to facilitate communication between the POS app and the Terminal App.

The following code snippets below outline the principle of delegation of the *Sale *transaction request to the Terminal App alongside the processing of a response delivered when the transaction is finished.

private void processSaleRequest(Integer amount) {
    TerminalAPI.setCredentials("api-userName:api-password");
    Map<FieldName, Object> params = new HashMap<>();
    params.put(TerminalReq.RequestType, RequestType.SALE);
    params.put(TerminalReq.Amount, amount);
    params.put(TerminalReq.TransactionCode, "0000000001");
    TerminalAPI.exec(this, params);
}

// In TerminalAPI class
public static void exec(Activity activity, Map<FieldName, Object> params, Integer requestCode) {
    try {
        Intent intent = locateTerminalAppApplication(activity);
        // Validations skipped
        params.putAll(defaults);
        intent.putExtra(Fields.PARAMS, convertRequestMap(params));
        intent.putExtra(Fields.CREDENTIALS, credentials);
        intent.setFlags(0);
        activity.startActivityForResult(intent, requestCode);
    } catch (Exception e) {
        throw UniPayException.s20(e);
    }
}

public class TerminalReceiver extends BroadcastReceiver {
    private static final String ACTION_TERMINALAPP_RESPONSE = "ACTION_TERMINALAPP_RESPONSE";
    private static final String ACTION_TERMINALAPP_STATUS = "ACTION_TERMINALAPP_STATUS";

    @Override
    public void onReceive(Context context, Intent intent) {
        SharedPreferences prefs = context.getSharedPreferences(context.getPackageName() + "_preferences", MODE_PRIVATE);
        String action = intent.getAction();
        if (action != null) {
            switch (action) {
                case ACTION_TERMINALAPP_STATUS:
                    //...
                    break;
                case ACTION_TERMINALAPP_RESPONSE:
                    String responseStr = intent.getStringExtra("data");
                    if (responseStr != null) {
                        try {
                            PayResult payResult = new PayResult();
                            TransactionResponse transactionResponse = TransactionResponse.fromJSON(responseStr);
                            String responseCode = transactionResponse.getResponseCode();
                            String returnStr = transactionResponse.getResponseMessage();
                            if (responseCode != null && (responseCode.equals("A01") || responseCode.equals("A02"))) {
                                payResult.approved = true;
                                payResult.reciept = prefs.getString("merchant_name", "") + "\n" + prefs.getString("merchant_regno", "") + "\n\n" + emvData;
                            } else {
                                // ...
                            }
                        } catch (Exception e) {
                            // Handle exception
                        }
                    }
                    break;
                default:
                    break;
            }
        }
    }
}

Note! Pseudo code above demonstrates principles of handling transaction response with a support of Broadcast Receiver component and specific actions transmitted from a Terminal App. An alternative handling scenario would imply processing of data returned in activity onActivityResult callback as a result of startActivityForResult call initiated before.

Please refer to the Examples section for getting references to an example implementation of the POS Client Demo application.

Proxy Mode

Overview

Proxy mode is an advanced solution that takes managed mode to the next level. To use it, you must install a small Android app on the payment device where Terminal App lives and transfer the data from the payment application into a TCP/IP socket stream. This allows the cash register (POS) to communicate with the payment device over four different network carriers: Wi-Fi, wired Ethernet, Bluetooth tethering, and USB cable tethering. Bluetooth and USB cable tethering also allow sharing the 4G mobile connection with the cash register, which is perfect for situations where DSL or fiber optic connections are not accessible.

The proxy mode uses an application that listens for any connection on a specific port. Whatever comes in the socket is relayed to the Intent, just like in the managed mode example. The result coming back from the intent is then sent back to the calling cash register’s (POS) TCP/IP socket. The data format of the data sent and received will be the same as in managed mode, except conversion and delivery in JSON format.

The benefits of using proxy mode are:

  • Works independently of network carriers.

  • Provides blazing-fast communication.

  • Allows the use of the payment device's screen as a marketing device, which can even be interactive.

  • Enables wiring the device to a PC with a USB cable for charging and data communication.

  • Ideal for 4G operation with USB or Bluetooth option.

  • More stable, as it does not require a stable internet connection.

However, there are some cons to consider:

  • Requires more development resources

  • Needs more initial configuration

The illustration below shows how the interactions between POS and Tidypay components take place in this mode:

Enter image alt description

Note! Proxy App becomes a responsibility of a POS integrator to develop and maintain. Even though it is a lightweight application, it has to support proper transfers of data structures using TCP/IP protocol from/to Intents data communicated with Terminal App.

Features:

  • High-speed transactions over local networking (Wi-Fi, USB, Bluetooth).

  • Ideal for high-volume retail or Electronic Cash Register (ECR) integrations.

Technical considerations

Data is formatted as JSON and transmitted via a socket to the terminal.

An example below contains a pseudo code that forms a Sale request:

private void processSaleRequestProxy(Integer amount) {
    Map<FieldName, Object> params = new HashMap<>();
    params.put(TerminalReq.RequestType, RequestType.SALE);
    params.put(TerminalReq.Amount, amount);
    Map<String, String> map = convertRequestMap(params);
    new Thread(new SendReceiveIP(new JSONObject(map).toString())).start();
}

Note! It is worth pointing out that the example above resembles to a large extent the code for managed mode. And this is true as a bulk of differences lie in the conversion of a map to JSON and then transporting it via TCP/IP routine.

BroadcastReceiver Integration

This component is essentially a back channel for pushing Terminal App transaction updates down to the POS system.

BroadcastReceiver's goal is to subscribe to notifications coming from the Terminal App and react correspondingly in the POS system.

Two types of actions/notifications are supported:

Transaction Responses

  • Action Name: ACTION_TERMINALAPP_RESPONSE

  • Content: Includes transaction response details in JSON format similar to the API server response.

Enter image alt description

Status Notifications

  • Action Name: ACTION_TERMINALAPP_STATUS

  • Content: Carries information about a certain screen displayed on the terminal

  • Data format:

FieldDescriptionPossible Values
screenCodeThe screen displayed in the Terminal App'present-card', 'enter-pin', 'select-application', 'visualize-result', 'provide-signature', 'confirm-signature'
entryModePossible payment methods'tap', 'insert', 'swipe', 'manual', 'tap|insert', 'tap|swipe', 'tap|manual', 'insert|swipe', 'insert|manual', 'swipe|manual', 'tap|insert|swipe', 'tap|insert|manual', 'tap|swipe|manual', 'insert|swipe|manual', 'tap|insert|swipe|manual'
screenMessageTitle or message on the screen.The parameter corresponds to the text of the current screen except for the screen Tap, Insert or Swipe

Code Example

public class TerminalAppResponseReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        String response = intent.getStringExtra("response");
        Log.d("TerminalResponse", response);
    }
}

Comparison of Integration Modes

FeatureManaged ModeProxy Mode
Ease of UseMediumLow
Device DependencyYesNo
Transaction SpeedFastFast
StabilityVery HighHigh
Setup ComplexityEasyMedium
Support RequirementsLowMedium
FlexibilityMediumVery High

Cross-cutting concerns

API Authentification

In each API call userName and password shall be provided.

The respective credentials shall be requested from Tidypay.

The password to be provided is static and is not rotated.

Identification of the transactions

Transaction Code parameter is recommended to be used for identification of the transaction within an external system. e.i. POS, that submits the transaction for processing. This allows for cross-referencing of entities between the Tidypay system and a submitter’s (POS) system.

This allows POS to request information about the transactions or make commands using *Transaction Code *when sending to Tidypay rather than using internal *TransactionID *(which is a Tidypay identifier). This, in-turn, also ensures that the POS and Tidypay can be reconciled properly.

Duplicate Transaction handling

The Tidypay system supports different policies of how Sale transactions behave in case the same Transaction Code is passed.

Enter image alt description

Note! "Return existing transaction" option is recommended. This way, the POS will always receive the 'correct' response, including if it turns out to be a duplicated transaction.

Let’s review scenarios when POS conducts two real-time transactions through usage of *Transaction Code *under different Transaction Duplicated Policy settings applied.

Let's assume that the first transaction (TR1) with a certain transactionCode (TC) was posted using the Sale API. We will check how the system processes a request to post a second transaction (TR2) with the same transactionCode (TC), considering the status of the first transaction (TR1): Approved, Declined, Voided, or Refunded.

Setting: No duplicates check = ON

Case #PreconditionTestResult
1TR1 = ApprovedPOST requestType=Sale with the same TCTR2 is posted:
responseCode=A01
responseMessage=Approved
2TR1 = DeclinePOST requestType=Sale with the same TCTR2 is posted:
responseCode=A01
responseMessage=Approved
3TR1 = VoidPOST requestType=Sale with the same TCTR2 is posted:
responseCode=A01
responseMessage=Approved
4TR1 = RefundPOST requestType=Sale with the same TCTR2 is posted:
responseCode=A01
responseMessage=Approved

Note! If there are multiple transactions with the same Transaction Code, then the requestType=find returns the ID of the most recently posted transaction.

Setting: Decline if duplicate = ON

Case #PreconditionTestResult
1TR1 = ApprovedPOST requestType=Sale with the same TCresponseCode=V29
responseMessage=
The transaction you are trying to process is a duplicate of a recently processed transaction
2TR1 = DeclinePOST requestType=Sale with the same TCresponseCode=V29
responseMessage=
The transaction you are trying to process is a duplicate of a recently processed transaction
3TR1 = VoidPOST requestType=Sale with the same TCresponseCode=V29
responseMessage=
The transaction you are trying to process is a duplicate of a recently processed transaction
4TR1 = RefundPOST requestType=Sale with the same TCresponseCode=V29
responseMessage=
The transaction you are trying to process is a duplicate of a recently processed transaction

Setting: Return existing transaction = ON

Case #PreconditionTestResult
1TR1 = ApprovedPOST requestType=Sale with the same TCTR1 is returned in the response
2TR1 = DeclinePOST requestType=Sale with the same TCresponseCode=V29
responseMessage=The transaction you are trying to process is a duplicate of a recently processed transaction
3TR1 = VoidPOST requestType=Sale with the same TCresponseCode=V29
responseMessage=The transaction you are trying to process is a duplicate of a recently processed transaction
4TR1 = RefundPOST requestType=Sale with the same TCresponseCode=V29
responseMessage=The transaction you are trying to process is a duplicate of a recently processed transaction

Note! requestType=find through Transaction Code returns the transaction data regardless of the payment result.

Last Updated:
Contributors: VECTOR-SOFTWARE\pkrupey