API Overview
The Payment Flow Service API
enables applications to get called at various stages in a flow and read and/or augment flow data.
We refer to these applications as flow services, of which there are two types - value added applications and payment applications.
There are a few basic requirements a flow service must fulfill in order to be accepted by FPS.
- Implement a content provider and expose a PaymentFlowServiceInfo model with relevant app information
- Implement one or multiple services which will be called in a flow
- Interact with one or multiple stage models to read and augment data
- Expose relevant components in the application manifest
Exposing application info
In order for your application to be AppFlow compliant, it must expose certain information. It does this via implementing a content provider and exposing it via the application manifest.
Adding the service info provider
FPS and other applications require certain information about every payment application.
This is done by extending BasePaymentFlowServiceInfoProvider
, as the sample code below shows
public class PaymentServiceInfoProvider extends BasePaymentFlowServiceInfoProvider {
@Override
protected PaymentFlowServiceInfo getPaymentFlowServiceInfo() {
return new PaymentFlowServiceInfoBuilder()
.withVendor("TheVendorName")
.withDisplayName("Name of your application for UI presentation")
.withCanPayAmounts(true, <supportedPaymentMethods>)
.withSupportedFlowTypes(FlowTypes.SALE, FlowTypes.REFUND, ...)
.withCustomRequestTypes(<Any custom types you may support>)
.withSupportedCurrencies(<list of supported currencies>)
.withDefaultCurrency(<optional default currency>)
.withMerchants(<Optional list of Merchant objects>)
.withManualEntrySupport(<whether or not MOTO is supported>)
.withSupportsAccessibilityMode(<whether or not you support visually impaired>)
.build(getContext());
}
@Override
protected boolean onServiceInfoError(@NonNull String errorType, @NonNull String errorMessage) {
switch (errorType) {
case RETRIEVAL_TIME_OUT:
Log.d(TAG, "Retrieval of service info timed out");
break;
case INVALID_STAGE_DEFINITIONS:
Log.d(TAG, "Problems with stage definitions: " + errorMessage);
break;
case INVALID_SERVICE_INFO:
Log.d(TAG, "Invalid service info: " + errorMessage);
break;
}
return true;
}
}
Returning PaymentFlowServiceInfo
See PaymentFlowServiceInfoBuilder for complete details of what can be set and the implementation in the Payment Service Sample
.
The following data must be set for payment applications:
withVendor()
- The company/organisation providing the servicewithDisplayName()
- The name of the application to show to merchants/customerswithCanPayAmounts()
-true
together with what payment methods are supportedwithSupportedFlowTypes()
- Define what flow types (payment functions) are supported (such assale
orrefund
). (details further down)
In addition, it is strongly recommended that the following data is set if possible:
withSupportedCurrencies()
- Restrict what currencies are supported (default is all currencies)withDefaultCurrency()
- Set the default currencywithManualEntrySupport()
- Whether or not manual entry / MOTO is supportedwithSupportsAccessibilityMode()
- Whether or not there is support for visually impaired users
See Bespoke Data for details.
Errors
Via the optional, but highly recommended, overriden method onServiceInfoError
, your flow service can receive details of when your flow service is rejected for some reason. See ServiceInfoErrors for a list of possible error types.
Expose via manifest
The provider needs to be exposed in the application manifest as follows
<provider
android:name=".PaymentServiceInfoProvider"
android:authorities="<your_custom_authority>"
android:exported="true">
<intent-filter>
<action android:name="com.aevi.sdk.flow.action.PROVIDE_SERVICE_INFO"/>
</intent-filter>
</provider>
Notifying data changes
If the data reported via PaymentFlowServiceInfo
has changed, you need notify FPS of this.
Call BasePaymentFlowServiceInfoProvider.notifyServiceInfoChange()
statically and FPS will invalidate any cached configuration data for your application and call your provider again to get the new information.
Implementing services
Now that you have decided what stages are relevant for your application and set up the content provider, it is time to implement the actual service. The service (which is an Android Service) is what FPS will be calling into for passing flow data and how your application can send back a response.
Determining when to get called
Depending on the type of payment application, different flow stages are relevant.
All payment applications must support the TRANSACTION_PROCESSING
stage, which is the point at which money is transferred during a payment flow. As an example. for a sale
flow, it is when the customer pays the merchant via some payment method such as card or cash.
AppFlow does not dictate what payment methods a payment application uses, as long as it accurately reports the correct payment method(s) in the app info and transaction responses.
Payment card reading
For traditional payment applications using payment cards, there is an optional (but recommended) stage called PAYMENT_CARD_READING
.
Read more about this here.
Non-payment functions
For other functions, such as reversal
(void
), tokenisation
, etc, the GENERIC
stage is used.
See Flow Types for the different generic types and information on what data is passed in the request and what is required in the response. The generic scenarios are explained in more detail at the end of this page.
The service
public class PaymentService extends BasePaymentFlowService {
@Override
protected void onPaymentCardReading(CardReadingModel model) {
// TODO handle
}
@Override
protected void onTransactionProcessing(TransactionProcessingModel model) {
// TODO handle
}
@Override
protected void onGeneric(GenericStageModel model) {
// TODO handle
}
}
Below is an example from the Payment Service Sample of how to implement a service that exposes the three stages.
Expose in manifest
<service
android:name=".service.PaymentService"
android:exported="true">
<intent-filter>
<action android:name="com.aevi.sdk.flow.action.PROCESS_TRANSACTION"/>
<action android:name="com.aevi.sdk.flow.action.READ_PAYMENT_CARD"/>
<action android:name="com.aevi.sdk.flow.action.PROCESS_GENERIC"/>
</intent-filter>
</service>
Transaction processing
The request for a payment flow comes in the form of a TransactionRequest
object and can be fetched via TransactionProcessingModel.getTransactionRequest()
. There are two different ids defined in this class - please see Transactions for more information about what the ids are how they can be used.
You can determine what type of transaction it is from TransactionRequest.getFlowType()
. This will map to one of the types defined in Flow Types for where the request class if of type Payment
.
The amounts relevant for the transaction are available via the TransactionRequest.getAmounts()
method. Note that it is very important to understand the way AppFlow represents amounts and what the structure of it means for a payment application. See parsing amounts for details.
It is possible that TransactionRequest.getCard()
contains some form of card data either if a card was read in the payment card reading stage or the POS app provided a card token to use for payment. You can check if the Card
object contains any data via the Card.isEmpty()
method, or check if a token is set via the Card.getCardToken()
method.
In addition, depending on the use cases for your payment application, the following information may also be available:
- Basket data (via
getBasket()
) - Customer data (via
getCustomer()
) - Custom/bespoke data (via
getAdditionalData()
)
Building the transaction processing response
When your payment app has finished processing the payment and determined the outcome, you should construct an appropriate TransactionResponse
object using the TransactionResponseBuilder
retrieved via TransactionProcessingModel.getTransactionResponseBuilder()
.
Transaction outcome
At a minimum, the outcome must be set via calling either one of the approve methods or the decline method on the builder. If a specific response code is supported via a defined protocol, such as ISO-8583, this should be set via the withResponseCode()
method. In addition, free text information about the outcome can be provided via the withOutcomeMessage()
method.
Processed amounts
If your payment application (host/gateway) supports partial auth and such a scenario occurs, then you can of course not use the request amounts as they were. In this case, it is recommended that you first "fill up" the base amount with the amounts authorised, and then split any remaining amounts uniformly (as a fraction) of the requested additional amounts.
Card details
If the payment method used for the transaction was a payment card (and it was read in this stage and not in payment card reading stage), it is strongly recommended that the following fields at a minimum are populated in the Card
model and then set via withCard()
method:
- Card network / scheme
- Card entry method
- Card token
Transaction references
Your application can provide any arbitrary references related to the transaction in the response. This is particularily important for functions that are based on a previously completed payment, such as reversals/voids and pre-auth completions.
See Reversal and Pre-authorisation / Completion for details on how to manage these types.
This, and any other relevant information the payment application may have about a transaction can be added to the transaction references via the withReference()
method. See TransactionResponse References for details on defined additional values. You may also set any custom and bespoke references your payment application and/or host/gateway may have defined.
Send the response
The response is then sent back via calling TransactionProcessingModel.sendResponse()
. This method will generate the response from the builder and send it back to FPS.
Payment card reading
As for transaction processing, the request type is TransactionRequest
, which can be fetched via CardReadingModel.getTransactionRequest()
.
Setting the response
If this stage is supported, the expectation is that a Card
object is created and passed back on successful card reading. The card reading can have three outcomes, reflected by these three methods in CardReadingModel
approveWithCard(Card)
- If a card was read successfullyskipCardReading()
- If card reading should be bypassed in this stage (for whatever reason)declineTransaction(String)
- If card reading failed and transaction should be declined
All these methods above will also send the response straight back to FPS, so there is no need to call any further methods in the model to finalise.
All the various fields in Card
are optional, but for this to be useful for any purpose, we recommend the following fields at a minimum are populated:
- Card network / scheme
- Card entry method
- Card token
It should also be noted that it is expected that the payment application keeps track of what happened in this stage and acts accordingly in the processing stage later.
Generic scenarios
The GenericStageModel
provides the input request and a mechanism to send back a response. In terms of how to parse the request and create a response for any given type, please see the Flow Types page which details all the supported types.
A generic flow can be set to be processed in the background either via the flow configuration or via the request data. These flows are executed separately to (and possibly in parallel to) the normal foreground flows and may not require any user interaction. Your application may not launch any UI in a background flow.
Flow types such as reversals are common candidates for background processing.
Handling events
As of v2.1.0, the stage models allow flow services to subscribe to events.
FPS will send events to your flow service under various circumstances, such as
- Acceptance or rejection of a response your flow service sent
- To indicate that your flow service must finish processing
- To indicate that you should resume/restart your user interface
- To indicate that a merchant has requested the flow to be cancelled
All defined events and associated documentation can be found here.
In addition, any data keys to read out data from the FlowEvent
can be found here.
Subscribe to events
model.getEvents().subscribe(event -> {
switch (event.getType()) {
case FINISH_IMMEDIATELY:
// Stop all processing - FPS is no longer accepting a response
break;
case RESUME_USER_INTERFACE:
// Resume or restart any UI you may have launched previously
break;
case CANCEL_OR_RESUME_USER_INTERFACE:
// Since v2.2.4
// Cancel if possible/feasible, otherwise resume and inform user
break;
case RESPONSE_ACCEPTED:
// Response was accepted, hooray!
break;
case RESPONSE_REJECTED:
String rejectReason = event.getData().getStringValue(REJECTED_REASON);
// Response was rejected with reason above, :(
break;
default:
// Unknown event
break;
}
});
The base class for all stage models provides a getEvents()
method that clients can use to subscribe to events as per below.
Handling FINISH_IMMEDIATELY
Under certain scenarios, FPS will ask your flow service to finish up immediately. At this stage, it is too late to send a response.
This would happen if your flow service times out, or if the merchant chooses to skip your application or cancels the whole flow.
As this is driven by the merchant either being inactive for a long period or explicitly choosing to progress the flow, it is assumed that any existing interactions that has been done up to this point can be discarded.
Handling RESUME_USER_INTERFACE
If you have chosen to not send back a response and finish up if your activity is stopped/destroyed, it is possible you will receive this event if the merchant is trying to resume the flow.
How this event is handled is very much up to the implementation of your flow service. For simple cases (like the samples) where no state is kept in the activities themselves, the activity can simply be restarted.
For more complex scenarios where there are multiple views/pages/steps in the activity, we would recommend that you instead send back a response when your activity is stopped/destroyed. If that is not feasible, then you must ensure that you store state outside of the activity context so that the activity can resume from where it left off when restarted. This can add a fair amount of complexity, which is why we are generally advising against it.
Handling CANCEL_OR_RESUME_USER_INTERFACE
This event was added in version 2.2.4
.
There are situations where a merchant may attempt to cancel the current flow. This may not always be a feasible action if the application currently executing is busy processing. For such cases, the flow may be configured to allow delegation of such cancellation requests to the currently running flow service, which results in the CANCEL_OR_RESUME_USER_INTERFACE
event being sent.
The flow service should always cancel if that is a possible course of action. For a payment service, this means declining the transaction with a cancellation status code. If cancellation is not possible, due to host processing for instance, then the UI should be resumed to foreground and ideally notify the merchant of why the cancellation request was not fulfilled.
Handling response outcome
Your response will either be accepted or rejected by FPS. If you have augmented the flow data in some way and the response is rejected, it indicates that your application have violated some of the augmentation rules. Note that you are not able to perform any further actions with AppFlow at this stage and should as such not launch any user interface for the outcome.
As per the code samples above, you can retrieve the reason for the rejection from the event data. This will be a human readable message outlining the reason for the rejection. It is recommended you log/capture/upload this information for troubleshooting and analysis purposes.
Some examples of why a response may be rejected are:
- Action performed has not been marked in
PaymentFlowServiceInfo
- such as updating amounts without callingwithCanUpdateAmounts(true)
- Amounts are invalid - such as paid amounts being zero, or exceeding the requested amounts
- Trying to apply discounts to a non-existing basket
Receipts
Receipts can be printed directly from within your payment application if desired. However, it is recommended instead that the printing of the receipt is handled by a separate and specific receipt printing application. This means that the print handling code is not mixed up with your payment application and is instead separated out. This has the benefit of allowing the flow to handle the printing which, in turn provides the flexibility to use any printer on any device.
The payment application should ensure that all the required information is added to the TransactionResponse
references so that a receipt printing flow app can use the data later to print to a receipt. The addReference(String key, T... values)
or addReferences(AdditionalData fromReferences)
methods can be used to add any type of reference data your payment application produces. In particular the payment application should ensure that it adds references indicating any acquirer/bank specific details that may be a requirement to show on the receipt. For example it is usually required that the receipt show an acquirer specific merchant and terminal id.
It is also usually required that a payment application adds card specific details such as EMV parameters etc. These should be added by the payment application into the additional data object of the Card
object associated with the TransactionResponse
.