1. Documentation
  2. AppFlow - POS Applications
  3. API

API Overview

Flow initiating applications (such as POS apps) access AppFlow functionality via the Payment Initiation API.

Retrieving the PaymentClient

The entry point to the API is the PaymentApi class. From here you can retrieve the client object that your application can interact with for accessing AppFlow functions.

// You must check that the processing service is installed, and if not,
// show message to merchant to contact support
if (!PaymentApi.isProcessingServiceInstalled(context)) {
     // Show error message and exit
}
PaymentClient paymentClient = PaymentApi.getPaymentClient(context);

PaymentClient Overview

The PaymentClient exposes a number of methods which allows a client to:

  • Retrieve information about AppFlow and other applications integrated with AppFlow
  • Initiate flows
  • Query for responses to previously initiated flows (as of v2.1.0)
  • Subscribe to system events

The following pages will go over these in detail.

Querying for information

Your application can query the API for information that will help you to decide what kind of flows can be initiated and what data can be defined in the request.

This information is contained with the PaymentSettings model object which exposes useful information such as;

  • Installed flow services and associated metadata
  • Flow configurations
  • FPS settings
  • AppFlow settings
paymentClient.getPaymentSettings()
  .subscribe(paymentSettings -> {
         // Do something with the `PaymentSettings` object here
   });

In the Payment Initiation Sample, check out the System overview and List flow services sections to view information exposed by the API.

See Querying Code Snippets for details on what you can query for with associated code snippets.

Installed flow services

A PaymentFlowServices object can be obtained via paymentSettings.getPaymentFlowServices(), which can be used to query for collated information across all flow services, or from a specific flow service.

Information relevant for what the current AppFlow environment supports can be reviewed, such as:

  • Supported currencies
  • Supported payment methods
  • Custom request types
  • The functions of a flow service
    • What stages it supports
    • Whether it can update amounts
    • Whether it can pay amounts
    • And much more...

In addition, flow service metadata is available such as application name, vendor, version, etc.

This, in combination with the information from the next section, will help you determine what is possible on any given device.

Flow configurations

The FlowConfigurations model returned via paymentSettings.getFlowConfigurations() exposes information about all the active flows in FPS.

The flow configurations alone determine what functions are available for a client application, such as whether refund is supported or whether a sale can be split or not. These flow configurations are generally defined by AEVI and/or the acquirer/bank to match the requirements for the environment and/or enabled device in which they will be used.

It is assumed that most client applications will have a particular range of functions they support. This API is designed for this, allowing the client to:

  • Check whether a given flow type (such as "sale") is supported
  • Get the defined flows for that flow type (can be zero to many)
  • Check what stages are defined for a given flow
  • And more...

It is also possible to get all or stream all the available flow configs and apply Rx filtering on these to get the results required.

FPS Settings

The behaviour of Flow Processing Service (FPS) can to some extent be configured by acquirers and/or merchants.

Examples of such settings are

  • Various timeouts relating to flow execution
  • Feature toggles (such as currency conversion, multi-device, etc)
  • UI theming, show/hide controls, etc

See all settings here.

These settings can be retrieved via paymentSettings.getFpsSettings().

AppFlow Settings

As of v2.1.0, this also exposes some general settings that is intended to be useful for any application integrated with AppFlow.

This includes things like date and time formats, primary language/currency, etc.

See all settings here.

Flow initiation

There are two ways to initiate a flow via PaymentClient.

  • initiatePayment(Payment payment)
  • initiateRequest(Request request)

A Payment is used when some defined amount of money is being transferred as a result of the transaction. Examples are sale (purchase / payment), refund, pre-authorisation, etc. These all involve money being moved from one entity to another - customer to merchant or vice versa, as defined by the client application. The Payment model has been defined to support these standard point of sale scenarios.

A Request on the other hand can be used for a wide range of scenarios whether it involves money or not, such as tokenisation, batch closure, receipt or printing requests, reversals/voids, etc. In order to support these different types of functions, the Request / Response model pair was created to allow for fully custom data being passed in either direction. Via this approach, we also allow AppFlow applications to define their own custom request types, which they can advertise via a PaymentFlowServiceInfo for clients to review and initiate. All generic flows/requests can be set to be processed in the background, invisible to the users.

What data is relevant for any given request is defined by the flow type. Please see the documentation in Flow Types for a full list of types and associated data.

Check flow support

paymentSettings.getFlowConfigurations().isFlowTypeSupported("refund");

You can check if a particular flow type is supported via

paymentSettings.getFlowConfigurations().getFlowTypes();

Or, get a list of all the supported flow types

Initiate a payment

In order to make a payment, a Payment object needs to be built. This is done via using the PaymentBuilder class. At a minimum a Payment must have a flow type and amounts set. Depending on the type of flow being initiated, other parameters may be defined as well. See Flow Types for examples for each type.

See PaymentFragment in the Payment Initiation Sample for code sample and use the Initiate a payment section in the application to see it live.

Amounts amounts = new Amounts(1000, "GBP");

PaymentBuilder paymentBuilder = new PaymentBuilder()
        .withPaymentFlow("sale")
        .withAmounts(amounts);

Please note that the base amount set in Amounts should always be inclusive of tax (GST, VAT, etc).

If the flow name is known (in scenarios where there are multiple flows per type), it is recommended that the flow name is specifically given by using the following method to specify the exact flow required withPaymentFlow(type, name).

You can define and provide basket details, so that other applications in the flow can use that information to provide services, like applying item based discounts, or print as line items on a receipt.

Basket basket = new Basket("myBasket");
basket.addItems(new BasketItemBuilder()
      .withLabel("myBasketItem")
      .withCategory("itemCategory")
      .withAmount(500)
      .build());
paymentBuilder.withBasket(basket);

As per the Amounts base amount, note that the basket item amount must be inclusive of tax.

For further information about what data can be set in the basket or basket items, see Basket Docs and Basket Data.

paymentBuilder.withSplitEnabled(true);

You can ask for the payment to be split across multiple transactions. This is typically used to split a bill across multiple customers in a restaurant for example.

You can specify a particular payment method to be used, which may impact what payment apps are eligible and/or what the payment app does internally. Note that this should only be set if you know there are payment apps that can handle this method - as otherwise the payment may fail as there are no eligible apps. See Payment Methods for defined values.

paymentBuilder.withPaymentMethod(paymentMethod);

paymentBuilder.withCardToken(token);

If you have access to a card token for the payment application to use, you can provide it via

Customer customer = getCustomerDetails();
paymentBuilder.withCustomer(customer);

// Add some bespoke data (value can be of any type)
paymentBuilder.addAdditionalData("myDataKey", "myDataValue");

In general for payment requests, you can also specify customer details, and set any custom data you like.

// Assign the subscribe return value to a field and ensure to dispose in activity onDestroy()
paymentClient.initiatePayment(paymentBuilder.build())
  .subscribe(() -> {
      Log.i(TAG, "Payment accepted");
      finish(); // If in an activity, it should be finished at this stage
  }, throwable -> {
      if (throwable instanceof FlowException) {
          FlowException flowException = (FlowException) throwable;
          // Check flowException.getErrorCode() against ErrorConstants
          // Log flowException.errorMessage() for further detail
      } else {
          // Unexpected exception
      }
  });

Once you have finished building your payment, you can send it to FPS using the initiatePayment method and subscribe to receive information whether the request was accepted or rejected by FPS.

Initiate a generic request

Examples of generic requests are tokenisation or reversal, but they are also used for custom types and status updates.

See GenericRequestFragment in the Payment Initiation Sample for code sample and use the Initiate a non-payment request section in the application to see it live.

For these types of requests, all applications must know what data to expect for any given request type. Please see Flow Types for common requests and code snippets for creating requests and responses.

Request request = new Request(flowType);
request.setFlowName(flowName); // If the flow name is known, it should be set for multi-flow scenarios
request.addAdditionalData("dataKey", "dataValue");
request.setProcessInBackground(true / false); // True here means no UI is allowed
paymentClient.initiateRequest(request)
  .subscribe(() -> {
      Log.i(TAG, "Request accepted");
      finish(); // If in an activity, it should be finished at this stage
  }, throwable -> {
      if (throwable instanceof FlowException) {
          FlowException flowException = (FlowException) throwable;
          // Check flowException.getErrorCode() against ErrorConstants
          // Log flowException.errorMessage() for further detail
      } else {
          // Unexpected exception
      }
  });

Once you have finished building your request, you can send it to FPS using the initiateRequest method and subscribe to receive information whether the request was accepted or rejected by FPS.

Background processing

Generic flows can be processed in the background, which means that they are executed separately to (and possibly in parallel to) the foreground flows. Applications may not launch any UI in a background flow and the requests are submitted to a request queue which are processed one at a time. Status updates are by definition processed in this manner.

The main use case for a client to set this flag is to indicate that there is no need for any user interaction. As an example, a reversal may be set to process in the background as it may not require a user interface.

Initiate a custom request

AppFlow applications can define custom request types, for which FPS will create an ad-hoc flow configuration, allowing client applications to initiate them. These custom request types are initiated and handled in the same way as the generic requests. The main difference is how they are defined and documented. It is possible that no extra data is passed to or from the the handling application for a custom type. If however such data is required, see Bespoke Data for more information.

As previously explained, the way Android components work with lifecycles and that AppFlow is service driven, means that there is no guarantee an activity will still be alive to receive a response once the flow is completed.

This separation (in rx terms, a broken chain) should not be a major issue for client applications provided that separate activities are used for the initiation and the response handling. Our recommendation is that there is one activity responsible for initiating the flow and another activity responsible for handling the response launched from the response listener service. Any state that is required for both the activities can easily be managed via dependency injection, singletons, etc. Note that the responses contain the originating request data.

Handling responses

The first version of AppFlow provided responses back via the initiation Rx stream, but due to the nature of Android component lifecycles (especially on Oreo and up), there is never any guarantee that the activity / service that initiated a flow is still alive when it is time to send the response back. Activities in particular are almost guaranteed to be destroyed for any non-trivial flow, as most of the POS devices have very limited memory. To ensure that the client receives the response reliably, the response channel is separated from the initiation rx chain via calls to standard Android services defined in the client.

There are two types of responses

POS applications must define response entry points that are called at the end of a flow.

This can be done by extending and implementing one of the response listener services. Currently two base listener services are defined:

  • BaseResponseListenerService - to listen for Response objects resulting from a initial Request
  • BasePaymentResponseListenerService - to listen for PaymentResponse objects resulting from a Payment request

See PaymentResponseListenerService and ResponseListenerService in the Payment Initiation Sample for trivial implementation that simply start an activity to display the result.

Response service

Response services have two callback methods for responses - one for generic responses, such as for any of the defined flow types or custom request types, and one for responses from a status update flow. These are delivered separately as many clients may not be interested in responses from status updates.

public class ResponseListenerService extends BaseResponseListenerService {

  @Override
  protected void notifyGenericResponse(Response response) {
     // Handle generic response
  }

  @Override
  protected void notifyStatusUpdateResponse(Response response) {
     // Handle (if required) status update response
  }

  @Override
  protected void notifyError(String errorCode, String errorMessage) {
      // Map error code against one of `ErrorConstants`
  }
}

There is also a callback method for errors that may have occurred during a flow.

<service
  android:name=".ResponseListenerService"
  android:exported="true">
  <intent-filter>
      <action android:name="com.aevi.sdk.flow.action.PROCESS_RESPONSE"/>
  </intent-filter>
</service>

The service should be registered in your manifest as normal and should include the correct intent filter as shown below.

How to parse the response data and handle errors are covered in the next sections.

Parse responses

As mentioned, there are two types of responses depending on what type of request was initiated.

Responses

As the Request and Response data is bespoke to each type of request, there is no general information on how to parse these responses. See Flow Types to find detailed information on how to create requests and parse responses for each use case.

Payment Responses

The response to a Payment is returned in the form of a PaymentResponse object.

To investigate the outcome of the request, you can call response.getOutcome(). This will return a PaymentRequest.Outcome value that can be:

  • FULFILLED - This essentially means success - in that the full amounts defined in the request have been processed successfully
  • PARTIALLY_FULFILLED - This means that parts of the amounts were successfully processed, but not all of it. See further down for more info.
  • FAILED - No money was charged. See the FailureReason for more info (below).

In the case of the latter two options above, a FailureReason can be parsed to get a better idea of what went wrong:

  • NONE - No error (never used for FAILED)
  • CANCELLED - The payment was cancelled before it could complete
  • REJECTED - The request was rejected, either due to invalid data or because there was no payment service that could handle it
  • DECLINED - The payment service declined the payment (see transactionResponse.getResponseCode() to determine the reason)
  • TIMEOUT - The flow timed out due to application and/or merchant inactivity
  • ERROR - Some unexpected error caused the payment to fail

There is a convenience method, response.isAllTransactionApproved(), that will help you determine whether all transactions were approved or not. It is important to realise that it is possible for the outcome to be FULFILLED despite failed transactions. This is due to the way the payment flow operates, where a split app can split up the payment into multiple transactions, and it may choose to retry a transaction or move the balance forward, etc. The payment flow only cares about ensuring the amounts requested are processed - it does not care how that is done.

You can get the full list of transactions by calling response.getTransactions(). The Transaction contains all the relevant input data for that transaction. It also contains a list of TransactionResponse objects that were generated from flow services in the flow. This is often from payment applications, but may also be from value added services such as loyalty where rewards or points were used.

The TransactionResponse contains the following mandatory fields:

  • Outcome - Approved or Declined
  • Amounts processed - The amounts processed (may not equal the TransactionRequest amount)
  • Payment method - What method was used for payment (card, cash, etc)

In addition, it may optionally contains * Response code - The response code, which is payment service (protocol, region, etc) specific * Card - The details of the card used, if any, for the transaction * References - A set of transaction references

See here for more information about Transaction objects.

Partially fulfilled

Partially fulfilled outcomes should generally be rare, but can happen as a result of

  • the payment app declining after a flow service pays off amounts (such as via loyalty points)
  • a split transaction getting cancelled after at least one approved transaction
  • a host/gateway performing a partial auth
  • some unexpected error in the flow.

It is essential that your application can manage this outcome. There are a few different approaches to take in this scenario;

  • You can initiate a new request with the remaining balance
  • You can "give up" and cancel the payment. In the case of split transactions, you must ensure to reverse the transactions that were approved.

It is advisable to offer these options in your app to the merchant as he/she would be in the best position to make this decision.

Flow application trace

You can review what flow applications were called and what type of data they may have augmented, if any, as part of the payment processing or your request.

The PaymentResponse and Transaction models contains information about what data was augmented in the flow, by what app and in what stage.

See PaymentResponse.getExecutedPreFlowApp(), PaymentResponse.getExecutedPostFlowApp() and Transaction.getExecutedFlowApps() to retrieve this information on an overall flow or per transaction basis.

The below keys can be used to check what data was augmented via mapping to values in the FlowAppInfo.getAugmentedData() list.

Augmented data key Description
"amount" The amounts were augmented
"basket" An existing basket was augmented, or a new one was added
"customer" Customer details were augmented or added
"currency" The currency of a transaction was updated (currency conversion)
"payment" The payment data was augmented (via PRE_FLOW)
"cancelled" The flow was requested to be cancelled
"additionalData" Additional data was augmented or added for the request
"references" References were added to the response

Handling errors

Flow errors can come in via two channels:

  • Initiation rx stream onError
  • Response listener notifyError

Initiation errors

paymentClient.initiatePayment(payment).subscribe(() -> {
    // Request accepted
}, throwable -> {
    if (throwable instanceof FlowException) {
        FlowException flowException = (FlowException) throwable;
        handleFlowError(flowException.getErrorCode());
        Log.e(TAG, "Flow error: " + flowException.getErrorMessage());
    } else {
        // Handle unexpected exception
    }
});

When FPS receives a request, it will perform a range of validation on it and any associated flow, as well as ensure that it is currently able to process the request. Any subscriber to the initiate methods must ensure to add a consumer for any Throwable, as per below. All exceptions thrown by AppFlow will be of type FlowException, but it is of course possible that unexpected exceptions can occur as well.

The flowException.getErrorCode() returns a code that can be mapped against the constants in ErrorConstants, part of the API. See the table below for possible error codes for the initiation stage.

Error code Error description
notInstalled Sent if the processing service (FPS) is not installed on the device
busy Sent if the processing service (FPS) is already processing a request
unsupportedOperation Sent if an unsupported flow/request type/name is sent to the processing service
multipleConfigProviders Sent if there is more than one configuration provider installed on the device
noConfigProvider Sent if there is no configuration provider installed on the device
noAvailableFlows Sent if there are no flows configured, or FPS has rejected all of them
noFlowServices Sent if there are no flow services installed on the device
incompatibleApiVersion Sent if the client is integrated with a different major API version than the processing service
duplicateRequestId Sent if the id of the request has already been used by a previous request
invalidFlowIdentifier Sent if the flow type or name in the request is not valid
invalidRequest Sent if the request was malformed or failed validation
missingResponseListener Sent if the client has not defined the relevant response listeners
invalidMessageType Sent in the (rare case) that the message received is invalid or unknown
cancelFailed Sent if the (rare case) where processing service fails to cancel a flow as requested
resumeFailed Sent if the (rare case) where the processing service failed to resume a flow as requested by merchant

Response listener errors

@Override
protected void notifyError(String errorCode, String errorMessage) {}

Once FPS has accepted your request, it will complete the initiation stream and start processing the flow. After this point, if any errors occur, they will be sent back via the response listener notifyError method. Most outcomes at this point will lead to an actual PaymentResponse or Response being generated with relevant outcomes set, but in rare cases the flow can abruptly end due to misbehaving applications, device problems, etc. The errors are sent back to the method signature as per below;

In this case, the error code and message have been extracted from the FlowException and passed directly to the listener. The following error codes are relevant for the response listener. See ErrorConstants in the API for defined values.

Error code Error description
flowServiceError A general purpose error that will be sent for fatal failures in a flow service
stageNotSupported Sent if a flow service does not support the stage it has just been called for
configError Sent if the processing service cannot find a handler(s) for a stage
unexpectedError Something unexpected happened

Querying for responses

As of AppFlow v2.1.x you can query the API for Response and PaymentResponse objects for flows that have been completed. This can be useful if as a POS application you need to check the outcome of a particular Response or PaymentResponse and you application no longer has the data. It can also be used to obtain information should a terminal error have occurred and you need to recover the response data.

This can be done using the queryResponses and queryPaymentResponses methods of the PaymentClient.

ResponseQuery paymentResponseQuery = new ResponseQueryBuilder()
  .withMaxResults(100)
  .build();

PaymentClient paymentClient = PaymentApi.getPaymentClient(context);

paymentClient.queryPaymentResponses(paymentResponseQuery)
  .toList()
  .subscribe(paymentResponses -> {
      if (paymentResponses.isEmpty()) {
          // no responses returned that meet your query parameters
      } else {
          // read responses and do your work here
      }
  }, throwable -> {
      if (throwable instanceof FlowException) {
          // handle FlowException here
      } else {
          // handle ordinary exception
      }
  });

In order to make a query for responses a query must first be built using the ResponseQueryBuilder. This class allows you to define various parameters you can use to filter the results returned. For example the results can be restricted to certain flow types or a specific date range.

The responses are returned as an Rx stream. This can be converted to a list if required as in the example above.

Events subscription

Any application can via the PaymentClient.subscribeToSystemEvents() subscribe to events from FPS. This method will return a stream of FlowEvent objects, each defined by a type. There are currently two main types of events defined:

  • FPS configuration state changes
  • External state changes

The main purpose of providing these events is to allow clients to re-query for relevant data when it has changed. Most of this data is wrapped in the PaymentSettings object, meaning a call to paymentClient.getPaymentSettings() to update any such cached data would be required to ensure up-to-date information.

FPS configuration changes

Whenever there is a change to the flow configurations or the FpsSettings, an event with type flowStateChanged will be published. This event type has two associated data keys:

  • eventKeySettingsChanged - When the FpsSettings have changed
  • eventKeyFlowConfigsChanged - When the flow configurations have changed

External state changes

When something external to FPS has changed, such as a flow service being installed, un-installed or updated, or an additional device is connected or disconnected, an event with type externalStateChanged will be published. This event has two associated data keys;

  • eventKeyFlowServicesChanged - When flow services have changed
  • eventKeyDevicesChanged - When additional devices state has changed

Example

paymentClient.subscribeToSystemEvents().subscribe(flowEvent -> {
  String type = flowEvent.getType();
  AdditionalData data = flowEvent.getData();
  switch (type) {
    case "flowStateChanged":
      boolean flowConfigsChanged = data.getBooleanValue("eventKeyFlowConfigsChanged", false);
      boolean fpsSettingsChanged = data.getBooleanValue("eventKeySettingsChanged", false);
      break;
    case "externalStateChanged":
      boolean flowServicesChanged = data.getBooleanValue("eventKeyFlowServicesChanged", false);
      boolean devicesChanged = data.getBooleanValue("eventKeyDevicesChanged", false);
      break;
  }

Status Update Flows

In addition to the standard flows that are defined to perform a certain action, like a purchase, or generating a token, AppFlow also enables applications to provide status updates via flows. These status updates can be of any nature that involves passing some data to one or multiple applications.

Unlike the normal generic flows (such as tokenisation) where a single application handles the request and passes back a response with an outcome, these flows allow multiple applications to be called in sequence for the purpose of reading input data. The application can choose to not send any data back, or to pass back a set of references. FPS will collate all references from the applications and pass back in the final Response back to the client.

These flows are processed via a request queue in the background, separately from the standard flows. Applications in these flows may not launch any user interface - they must handle the update in the service only. Responses to status updates come in via the BaseResponseListenerService.notifyStatusUpdateResponse() callback method. If the client application is not interested in the responses, this method can be left empty.

See Flow Types for defined status update flows.