Skip to main content

Online payments

PayPal In-app

Last updated: 13-Mar-2024

Billing Agreement API

PayPal Initiate billing agreement

Merchant setup

This section describes the possible merchant setups and highlights crucial data for in-app transaction processing.

At the core of the structure is the merchant, who owns at least one store. Merchants and stores are represented by abstract entities. Usually, the merchant will configure one bank account at the merchant entity to receive the settlement. However, it is possible to configure a separate bank account at each store as well. The settlement reports are in turn tied to the bank account(s), since they contain the breakdown of the received bank statement.

To enable the processing of in-app transactions, each store needs a Payment Provider Contract (PPC) linked to it, which is uniquely defined by the contract UUID. Each PPC contains a reference to the bank account which will be used for settlement of transactions processed with this contract.

To ensure proper reporting, the merchant (or the franchise) needs to maintain a mapping of the stores and PPC UUIDs, since the later one is needed during transaction processing. The same PPC UUID needs to be used for processing refunds, which alternatively can be processed from terminals connected to the store where the transaction was processed.

Merchant setup

In case the merchant is part of a franchise structure, there will be an additional corporation level in the hierarchy, below which the corporate owned merchant structure and any number of franchisee merchant structures are located.

Franchise setup

User management flows

It is assumed that the merchant already has an existing user management including, e.g., account creation and login inside the merchant app.

Billing agreement management

A billing agreement is needed to process in-app transactions. It is best practice to manage the billing agreement at the merchant backend and not store any sensitive data inside the merchant app.

Create Billing Agreement

The creation of the billing agreement is initiated by the user within the merchant app, e.g., during account setup, account configuration or before the first PayPal transaction.

Therefore, the merchant backend needs to initiate the billing agreement to receive the approvalUrl and the billingToken, which is a temporary reference until the final agreementId is created.

The user needs to be redirected to the approvalUrl to log into his/her PayPal account and confirm the billing agreement.

Once the user got redirected after the confirmation, the final step is to create the billing agreement and receive the agreementId, which needs to be saved on the merchant backend and linked to the user account.

Billing Agreement creation

Cancel Billing Agreement

The merchant app needs to provide an option to cancel the billing agreement linked to the user account.

The merchant backend then collects the required data to perform the cancel request towards the Verifone backend.

Once the successful response is received, the agreementId is deleted from the user account and the same is confirmed to the user inside the app.

Billing Agreement cancellation

Transaction processing with Billing Agreement

PayPal risk management

PayPal requires the integration of their Magnes SDK, which is a portion of PayPal's fraud and risk management. From the Magnes SDK description:

With direct access to the mobile application, Magnes accesses necessary information from the device and provides the data to PayPal Risk Services for early risk identification and mitigation. Magnes is implemented in the form of a library available for the Android and iOS platforms.

The high-level steps to integrate the Magnes SDK in PayPal transaction processing are shown below:

PayPal risk management

Further implementation details can be found in the PayPal Magnes SDK documentation.

Transaction processing

Transaction processing with Billing Agreement

Reference merchant app implementation

This section shows sample screens of the app, showcasing above flows for Billing Agreement management and transaction processing. Some code samples are provided as well.

Sample screens

The following sample screens show a transaction with seamlessly integrated creation of billing agreement:

1 Login
1. User login
2 Home
2. Home
3 Add to basket
3. Add item to basket
4 Basket
4. Basket
5 Payment
5. Checkout
6 PayPal login
6. PayPal login*
7 PayPal agreement
7. Billing Agreement confirmation*
8 PayPal thanks
8. Successful Billing Agreement*
9 Place order
9. Place order
10. Payment success
10. Payment result

*Steps 6, 7 and 8 are only needed when the first transaction is being processed. With a billing agreement in place, the flow jumps from 5 to 9 (e.g., when the second transaction is being processed).

Platforms for app development

App development options and limitations

There are several possibilities to develop an app to be run on Android and iOS:

  • separate development for each platform
    • Android
      • programming language: Kotlin, Java
      • UI: traditional imperative, declarative (Jetpack Compose)
      • communication libs: okhttp, ktor
    • iOS
      • Swift, Objective C
      • UIKit, SwiftUI
  • cross platform development
    • progressive web apps: HTML/CSS, React, Angular, Vue
    • hybrid: PhoneGap, Cordova
    • compile to native: Xamarin, NativeScript, React Native, Flutter, Kotlin multiplatform mobile

Only compile to native solutions create real apps found in-app stores and are running fast.

Solutions based on JavaScript (e.g., React Native) need additional wrappers for native libraries to be used (e.g., the required PayPal Magnes SDK). This is also true for Xamarin using C#.

Using Kotlin multiplatform mobile, it is necessary to develop UI twice; only non-UI code can be reused.

Development approach used for the reference merchant app implementation

It is not possible to find a solution, where the code can be reused by all possible integrators without change. To reduce effort, cross platform development has been chosen.

Using Flutter (https://flutter.dev/, https://en.wikipedia.org/wiki/Flutter_(software)), there are none of the above limitations. That is why it has been chosen as platform for the example merchant app. It is using Dart (https://dart.dev/, https://en.wikipedia.org/wiki/Dart_(programming_language)) as programming language. Thus, examples below are in this language (or GraphQL).

Communication to server

Beside OAuth2 for user login, GraphQL (https://graphql.org) is used for communication with merchant server. It is a query language for APIs similar to REST.

App is using Dart package GraphQL client (https://pub.dev/packages/graphql).

Billing Agreement management

Create Billing Agreement

App is sending the following GraphQL mutation for creation of billing agreement:

mutation CreateBillingAgreement

mutation CreateBillingAgreement($paymentOptionId: ID, $successUrl: String!, $cancelUrl: String!) {
  createBillingAgreement(paymentOptionId: $paymentOptionId, successUrl: $successUrl, cancelUrl: $cancelUrl) {
    id,
    url,
    state
  }
}

Parameters of mutation (successUrl, cancelUrl) are sent to PayPal to be used as return url after processing of PayPal website. In case of success, server returns a billing agreement (id, url, state).

An optional payment option can be specified.

Seamless confirmation of Billing Agreement

App launches web browser with url returned by server. Flutter plugin url_launcher (https://pub.dev/packages/url_launcher) is used:

launch url

BillingAgreement agreement = await createBillingAgreement();
if ((agreement != null) && (await canLaunch(agreement.url))) {
  await setAgreementId(agreement.id);
  await launch(agreement.url);
}

To return to the app at the receipt of url (deep link), the app must be configured according to operating system.

Android

Application manifest must include an intent filter for activity:

intent filter for deep link

<meta-data android:name="flutter_deeplinking_enabled" android:value="true" />
<intent-filter android:autoVerify="true">
  <action android:name="android.intent.action.VIEW" />
  <category android:name="android.intent.category.DEFAULT" />
  <category android:name="android.intent.category.BROWSABLE" />
  <data android:scheme="verishop" />
</intent-filter>

iOS

In an iOS application, the same is done by changing Info.plist:

Info.plist

<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleTypeRole</key>
    <string>Editor</string>
    <key>CFBundleURLName</key>
    <string>routes</string>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>verishop</string>
    </array>
  </dict>
</array>
<key>FlutterDeepLinkingEnabled</key>
<true/>

Back in the app, routing must be set according to received return url. Using Flutter, this is done independent from OS. onGenerateRoute is used in build function of app main widget.

flutter routing

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    ...
    onGenerateRoute: (settings) {
      final uri = Uri.parse(settings.name);
      if (uri.pathSegments.length == 2 && uri.pathSegments.first == 'agreement') {
        final action = uri.pathSegments[1];
        if (action == 'success' || action == 'cancel') {
          return MaterialPageRoute(builder: (context) => DashboardScreen(action: action));
        }
        if (action == 'transaction') {
          return MaterialPageRoute(builder: (context) => PlaceOrderScreen(action: action));
        }
        ...
      }
      ...
    }
  }
}

In this example, return url 'verishop://agreement/success' and 'verishop://agreement/cancel' are redirected to DashboardScreen. In case of successful billing agreement during transaction (url 'verishop://agreement/transaction'), it is routed to PlaceOrderScreen.

A successful billing agreement must be activated using following GraphQL mutation:

mutation ActivateBillingAgreement

mutation ActivateBillingAgreement($id: String) {
  activateBillingAgreement(id: $id) {
    id,
    state
  }
}

In case of success, the server returns the state 'ACTIVE'.

Cancel Billing Agreement

The app is sending the following GraphQL mutation for cancellation of billing agreement:

mutation CancelBillingAgreement

mutation CancelBillingAgreement($id: String) {
  cancelBillingAgreement(id: $id) {
    id,
    state
  }
}

In case of success, the server returns the state 'CANCELLED'.

Set Billing Agreement as default payment option

Billing agreements are stored on server for the user, as payment option (name, agreement). The option is returned to the app at user login and can be used during transaction without creating agreement again.

Transaction flow

Create Basket

On home screen, the app shows a list of items with title, description, picture, price, and tax.

items.dart

class SaleItem {
  String title;
  String description;
  String picture;
  int price;
  int tax;
  SaleItem(this.title, this.description, this.picture, this.price, this.tax);
  String getFormattedPrice([int quantity = 1]) => formatAmount(price * quantity);
}
 
List _items = [];

The item list is sent by server to the app.

Using Flutter for each item, a widget is built as one row of a list:

create row for item

Widget _buildRow(BuildContext context, int i) {
  final item = getSaleItem(i);
  return Padding(
    padding: const EdgeInsets.only(top: 10.0, bottom: 10.0),
    child: ListTile(
      leading: Icon(context, 'assets/images/${item.picture}'),
      title: Text('${item.title}', style: _biggerFont),
      subtitle: Text('${item.description}', style: _smallerFont),
      trailing: ElevatedButton(
        child: Text('\$ ${item.getFormattedPrice()}'),
        onPressed: () => _add2basket(context, item))));
}

Additional to picture, title, and description, it shows a button with price. Pressing this button adds the item to the basket.

The basket screen shows a list of basket items and total amount. Additionally, it redirects to checkout page, if the corresponding button is pressed.

Checkout screen

This screen shows payment option(s) returned by server. For each item in the list, an icon (if image exists in assets), name (e.g., PayPal) and checkbox are shown.

show payment option(s)

Widget _optionRow(BuildContext context, AppState model, int i) {
  final optionName = model.options[i].name;
  final imageFile = 'assets/images/${optionName.toLowerCase()}.png';
  return ListTile(
    title: Text('$optionName', style: biggerFont),
    leading: icon(context, imageFile),
    trailing: Checkbox(
      onChanged: (value) {
        setState(() {
          _activeOption = optionName;
        });
      },
      value: _activeOption == optionName,
    ),
  );
}

Active option is stored in the estate variable _activeOption. Corresponding checkbox is checked. In this case, no billing agreement must be created during transaction.

If no checkbox is checked, the user can select it. In this case, billing agreement will be created if the user presses Continue before switching to the "place order" screen.

select payment option

ElevatedButton(
  onPressed: model.isAgreementActive(_activeOption)
    ? () => Navigator.push(context, MaterialPageRoute(
        builder: (context) => PlaceOrderScreen(),
      ))
    : () => createAgreement(context, true, model.getOptionByName(_activeOption).id),
  child: Text('Continue'))

"Place order" screen

This screen shows transaction details (subtotal, tax, total, selected payment option, basket items) again before starting transaction via pressing place order.

In this case, the following GraphQL mutation is sent to merchant server:

transaction

mutation transaction($agreementId: ID!, $totalAmount: AmountInput!, $items: [ItemInput], $fraudId: ID) {
  transaction(agreementId: $agreementId, totalAmount: $totalAmount, items: $items, fraudId: $fraudId) {
    id,
    state
  }
}

fraudId is used for PayPal Magnes SDK, see below.

After processing, the app switches to payment result screen.

Payment result

This screen shows transaction result (success or error) and some transaction details.

Additionally, there is a button that redirects to Home Screen.

In case of successful transaction, the basket is cleared.

PayPal Magnes SDK

Magnes implements functions (setUp, collect, collectAndSubmit), which are used by merchant app:

  • setUp is called once at startup of application
  • collectAndSubmit is called during transaction. It returns the PayPal-Client-Metadata-Id which is sent as fraudId to the merchant server.

While SDK can be used directly for pure Android or iOS apps, using a platform independent framework makes it necessary to implement an abstraction layer. The solution for Flutter is based on messages, see https://flutter.dev/docs/development/platform-integration/platform-channels?tab=ios-channel-swift-tab. The following example is using MethodChannel for Dart part of Magnes setup:

Magnes setup

const platform = const MethodChannel('verishop.verifone.com/magnes');
 
Future<bool> magnesSetup() async {
  bool result = false;
  try {
    result = await platform.invokeMethod('setup');
  } on PlatformException catch (e) {
    print('Failed to setup magnes: ${e.message}');
  }
  return result;
}

Reference merchant backend implementation

Merchant server is implemented using following technologies:

  • programming language TypeScript
  • node.js with express framework
  • Mongo DB
  • GraphQL
  • OAuth2

eComm API

This documentation does not describe all details of implementation. The main focus is on the description of integration for PayPal Ecomm API provided by Verifone.

Some TypeScript code fragments using node.js express and bent are provided as examples.

Billing agreement

Creation

To create a new billing agreement, send a POST to the endpoint '/billingAgreement/initiate'. The Parameters of payload are payment provider contract id (ppcId) and URLs for return and cancel. An additional authorization header for basic authentication is necessary.

const post = bent('POST', 'json', 201, {
  authorization: paymentOption.auth
}, paymentOption.baseUrl)
const obj = await post('/billingAgreement/initiate', {
  paymentProviderContract: paymentOption.ppcId,
  returnUrl: successUrl,
  cancelUrl: cancelUrl
})

A successful result will include approvalUrl and billingToken

Activation

The endpoint for activation of an agreement is '/billingAgreement/create'. The payload should include parameters ppcId and billingToken received on creation.

const post = bent('POST', 'json', 201, {
  authorization: option.auth
}, option.baseUrl)
const obj = await post('/billingAgreement/create', {
  paymentProviderContract: option.ppcId,
  billingToken: agreement.token,
})

The result message includes parameter state which should be 'ACTIVE'.

Cancellation

The endpoint for cancellation is '/billingAgreement/$id/cancel'. ppcId is necessary as parameter in the payload.

const post = bent('POST', 'buffer', 200, {
  authorization: option?.auth
}, option.baseUrl)
await post(`/billingAgreement/${agreement.agreementId}/cancel`, {
  paymentProviderContract: option.ppcId
})

Transaction

Messages for transactions should use the '/transactions' endpoint. The payload should include ppcId, billingAgreementId and a transaction object (amount, items, etc.). Additional headers x-vfi-api-idempotencyKey and paypalFraudId are necessary.

transaction request

const headers: Record<string, string> = {
  authorization: option.auth,
  'x-vfi-api-idempotencyKey': uuidV4(),
  paypalFraudId: fraudId
}
const post = bent('POST', 'json', 201, headers, option.baseUrl)
const initObj = await post('/transactions', {
  paymentProviderContract: option.ppcId,
  billingAgreementId: agreement.agreementId,
  ...transObj
})

Example of an API used between merchant app and backend

The complete API of the merchant server as GraphQL schema:

type Query {
  merchant: Merchant!
  merchants: [Merchant]
  user: User!
  users: [User]
}
 
type Mutation {
  createBillingAgreement(paymentOptionId: ID, successUrl: String!, cancelUrl: String!): BillingAgreement!
  activateBillingAgreement(id: ID!): BillingAgreement!
  cancelBillingAgreement(id: ID!): BillingAgreement!
  transaction(agreementId: ID!, totalAmount: AmountInput!, items: [ItemInput], fraudId: ID): Transaction!
  createUser(merchantId: ID, name: String!, password: String!): User!
  createMerchant(name: String!): Merchant!
  createPaymentOption(merchantId: ID, name: String!, baseUrl: String, auth: String, ppcId: String): PaymentOption!
  createSaleItem(merchantId: ID, title: String!, description: String, picture: String, price: Int!, tax: Int): SaleItem!
  removeSaleItem(title: String!): Boolean!
}
 
input AmountInput {
  value: Int!
  currency: String!
}
 
input ItemInput {
  name: String!
  unitAmount: AmountInput!
  taxAmount: AmountInput!
  quantity: Int
}
 
type Amount {
  value: Int!
  currency: String!
}
 
type Item {
  name: String!
  unitAmount: Amount!
  taxAmount: Amount!
  quantity: Int
}
 
enum State {
  PENDING,
  ACTIVE,
  CANCELLED
}
 
scalar Date
 
type PaymentOption {
  id: ID!
  name: String!
  billingAgreement: BillingAgreement
}
 
type BillingAgreement {
  url: String
  state: State
  id: ID!
}
 
type Transaction {
  id: ID!
  created: Date!
  state: String
  totalAmount: Amount!
  items: [Item]
}
 
type Merchant {
  id: ID!
  name: String!
  users: [User]
  saleItems: [SaleItem]
}
 
type SaleItem {
  title: String!
  description: String
  picture: String
  price: Int!
  tax: Int!
}
 
type User {
  id: ID!
  name: String!
  paymentOptions: [PaymentOption]
  transactions: [Transaction]
}

Besides queries (one or all merchants or users), it includes mutations used by the merchant app: create, activate and cancel billing agreement, transaction. Other mutations are used for server administration: create user, create merchant, create payment option, create, and remove sale item.

Need help?

Do you have a question? If you didn’t find the answer you are looking for in our documentation, you can contact our Support teams for more information. If you have a technical issue or question, please contact us. We are happy to help.

Not yet a Verifone customer?

We’ll help you choose the right payment solution for your business, wherever you want to sell, in-person or online. Our team of experts will happily discuss your needs.

Verifone logo