Send Cross Border Payments

Learn how to send cross border payments on behalf of your customers.

Overview

Performing PAPE cross-border payments on behalf of your users is a two step process.

  1. First you create a payment-intent. PAPE will return rates, fees, and name verification checks.
  2. Secondly, you perform a payment, with the parameters obtained in step 1, decorated with additional security measures.

Note that you can only specify the recipient amount, not the sender amount. The expected sender amount will be calculated and returned back to you in the api response.

Only payment gateways can initiate payment intents and payments requests.

Data Models

Payment Intent

fieldtypedescription
senderGatewayIdstringPayment Gateway ID of the sender
senderCurrencystringCurrency used by the sender
senderAccountstringSender account
receiverGatewayIdstringPayment Gateway ID of the receiver
receiverAccountstringReceiver account
receiverCurrencystringCurrency used by the receiver
receiverAmountstringAmount in receiver currency the destination will receive
memostringmemo associated with the payment

Payment

fieldtypedescription
senderGatewayIdstringPayment Gateway ID of the sender
senderCurrencystringCurrency used by the sender
senderAccountstringSender account
receiverGatewayIdstringPayment Gateway ID of the receiver
receiverAccountstringReceiver account
receiverCurrencystringCurrency used by the receiver
receiverAmountstringAmount in receiver currency the destination will receive
memostringmemo associated with the payment
senderAmountstringAmount sender has to pay, excluding sender fees
senderNamestringName of the sender
receiverNamestringName of the receiver
receivingGatewayFeestringAmount in receiver currency charged by the receiving gateway
senderGatewayTransactionIdstringTransaction id generated by the sending gateway when sending a payment
receiverGatewayTransactionIdstringTransaction id generated by the receiving gateway when honoring a payment
senderGatewayStatusstringStatus of the payment according to the sending gateway
receiverGatewayStatusstringStatus of the payment according to the receiving gateway
blockchainTransactionIdstringBlockchain transaction ID of the payment

API Calls

Endpoints

staging: https://apis-alpha.dev.projectwhite.io

Responses
http codecontent-typeresponse
200application/jsonPayment Intent Object. (see data model above)
4xxapplication/json{"error": "string","message": "string"
Endpoints

staging: https://apis-alpha.dev.projectwhite.io

Responses
http codecontent-typeresponse
200application/jsonPayment Intent Object. (see data model above)
4xxapplication/json{"error": "string","message": "string"

Try it out:

Let’s send funds from fritz*cm.wallet.interstellar-demo.xaf to ernest*ng.wallet.interstellar-demo.ngn

Tools you might need

These are the tools you might need to follow along this tutorial.

  • linux or mac: Many of the command line examples have been customized for UNIX-like operating systems.

  • curl: To perform API calls.

    • mac: brew install curl
    • ubuntu/debian: sudo apt-get install curl
  • jq: To process json on the command line.

    • mac: brew install jq
    • ubuntu/debian: sudo apt-get install jq
  • jo: To create json on the command line.

    • mac: brew install jo
    • ubuntu/debian: sudo apt-get install jo
  • openssl: To perform cryptographic operations on the command line.

    • mac: brew install openssl
    • ubuntu/debian: sudo apt-get install openssl

RSA Keys

Depositing funds requires digital signatures as previously outlined in our Security Prologue.

To facilitate our command line operations for this tutorial, we will store our payment gateway RSA keys in special folders.

mkdir -p ~/.pape-rsa-keys

The private and public keys of each payment gateway you manage should be kept inside the above folder with the format below:

  • ~/.pape-rsa-keys/paymentGatewayId.pub for the public key, and
  • ~/.pape-rsa-keys/paymentGatewayId.priv for the private key

e.g.

  • ~/.pape-rsa-keys/ng.bank.ecobank.ngn.pub for the public key, and
  • ~/.pape-rsa-keys/ng.bank.ecobank.ngn.priv for the private key

Get Token

export PAPE_TOKEN_ENDPOINT=https://id.dev.projectwhite.io/auth/realms/projectwhite/protocol/openid-connect/token;

export PAPE_API_ENDPOINT=https://apis-alpha.dev.projectwhite.io
export PAPE_USERNAME=cm.wallet.interstellar-demo.xaf;
export PAPE_PASSWORD=abc;
export PAPE_TOKEN=`curl -s --request POST \
--url "$PAPE_TOKEN_ENDPOINT" \
--data-urlencode grant_type=password \
--data-urlencode username=$PAPE_USERNAME \
--data-urlencode password=$PAPE_PASSWORD \
--data-urlencode 'client_id=projectwhite' \
| jq -r ".access_token"`

echo $PAPE_TOKEN

Construct Payment Intent JSON Payload

export PAPE_API_PAYLOAD=`jo -p -- \
senderGatewayId=$PAPE_USERNAME \
senderCurrency=XAF \
senderAccount=fritz \
receiverGatewayId=ng.wallet.interstellar-demo.ngn \
receiverAccount=ernest \
receiverCurrency=NGN \
-s memo="school fees" \
-s receiverAmount="1000"`

echo $PAPE_API_PAYLOAD

Call Payment Intent Creation API

export PAPE_PAYMENT_INTENT_RESPONSE=`curl -s --request POST \
--url "$PAPE_API_ENDPOINT/payment-intents" \
--data "$PAPE_API_PAYLOAD" \
--header  "accept: application/json" \
--header 'accept: */*' \
--header "Content-Type: application/json" \
--header "Authorization: Bearer $PAPE_TOKEN"`;

echo $PAPE_PAYMENT_INTENT_RESPONSE | jq .

Sample Response

{
  "id": "6174094f-6b56-45ac-8c7b-688b511e0474",
  "senderGatewayId": "cm.wallet.interstellar-demo.xaf",
  "senderAmount": "1442.25",
  "senderCurrency": "XAF",
  "senderAccount": "fritz",
  "senderName": "Interstellar XAF 12345",
  "receiverGatewayId": "ng.wallet.interstellar-demo.ngn",
  "receiverAmount": "1000",
  "receiverCurrency": "NGN",
  "receiverAccount": "ernest",
  "receiverName": "Interstellar NGN 12345",
  "receivingGatewayFee": "100",
  "memo": "school fees",
  "extras": null
}


Note how the sender amount is returned above. Front-ends can determine exchange rates by dividing the receiver amount from the sender amount.

Once the user has confirmed the sender and receiver names are correct, and that the exchange rates are acceptable to the user, the payment gateway should proceed to extract the expected amount from the sender account. Including whatever sending fees the payment gateway wants to charge.

Once that is done, the payment gateway should confirm payment has been made to PAPE using the calls below.

Construct Payment JSON Payload

The payment (confirmation) endpoint payload is similar to the payment intent response obtained in the previous step.

You only need to add the core banking transaction id that was generated when the sender amount was extracted from the sender account.



export PAPE_PAYMENT_PAYLOAD=`echo $PAPE_PAYMENT_INTENT_RESPONSE | jq '. +=  {"senderGatewayTransactionId":"'$(date +%s)'"}'`;

echo $PAPE_PAYMENT_PAYLOAD

Sample Response

{
  "id": "168009ce-0d64-49f9-8b30-6ab481e38933",
  "senderGatewayId": "cm.wallet.interstellar-demo.xaf",
  "senderAmount": "1442.25",
  "senderCurrency": "XAF",
  "senderAccount": "fritz",
  "senderName": "Interstellar XAF 12345",
  "receiverGatewayId": "ng.wallet.interstellar-demo.ngn",
  "receiverAmount": "1000",
  "receiverCurrency": "NGN",
  "receiverAccount": "ernest",
  "receiverName": "Interstellar NGN 12345",
  "receivingGatewayFee": "100",
  "memo": "school fees",
  "extras": null,
  "senderGatewayTransactionId": "1674766017"
}

Notice the extra field: senderGatewayTransactionId.

The exchange rate of 1 NGN to XAF is approx: 1442.25/1000 = 1.44 XAF for one NGN.

To be more precise, the exchange rate should consider the receivingGatewayFee. Thus, 1 NGN to XAF is 1442.25/(1000 + 100) = 1.31 XAF for one NGN.

Get Signature


export PAPE_CALL_SIGNATURE=`echo -n "/payments$PAPE_PAYMENT_PAYLOAD" | openssl dgst -sha256 -sign ~/.pape-rsa-keys/$PAPE_USERNAME.priv | openssl enc -base64 -A`

echo $PAPE_CALL_SIGNATURE

Call Payment (Confirmation) API

curl -s --request POST \
--url "$PAPE_API_ENDPOINT/payments" \
--data "$PAPE_PAYMENT_PAYLOAD" \
--header  "accept: application/json" \
--header "X-PAPE-SIGNATURE-BASE64: $PAPE_CALL_SIGNATURE" \
--header 'accept: */*' \
--header "Content-Type: application/json" \
--header "Authorization: Bearer $PAPE_TOKEN" | jq .

Voila !!!

Sample Response

{
  "id": "b815154ea937554dd89dfcc09828b05f3e4187749a11d2bd5cda3bc304751660",
  "createdAt": "2023-01-26T20:58:20.968720171Z",
  "updatedAt": "2023-01-26T20:58:20.968720171Z",
  "pagingToken": "1674766700968-1091061797",
  "senderGatewayId": "cm.wallet.interstellar-demo.xaf",
  "senderAmount": "1442.25",
  "senderCurrency": "XAF",
  "senderAccount": "fritz",
  "senderName": "Interstellar XAF 12345",
  "receiverGatewayId": "ng.wallet.interstellar-demo.ngn",
  "receiverProcessingGatewayId": "ng.wallet.interstellar-demo.ngn",
  "receiverAmount": "1000",
  "receiverCurrency": "NGN",
  "receiverAccount": "ernest",
  "receiverName": "Interstellar NGN 12345",
  "senderGatewayStatus": "processed",
  "receiverGatewayStatus": "pending",
  "senderGatewayTransactionId": "1674766017",
  "receiverGatewayTransactionId": "",
  "senderGatewayPublicKey": "",
  "receiverGatewayPublicKey": "",
  "blockchainTransactionId": "d17a929ffc9978a73d24dd44eb655aeb78118a1245dd05e91f89b4f7d1f688fd",
  "receivingGatewayFee": "100",
  "memo": "school fees",
  "creationRequestUrl": "/payments",
  "creationRequestBody": "{\n  \"id\": \"168009ce-0d64-49f9-8b30-6ab481e38933\",\n  \"senderGatewayId\": \"cm.wallet.interstellar-demo.xaf\",\n  \"senderAmount\": \"1442.25\",\n  \"senderCurrency\": \"XAF\",\n  \"senderAccount\": \"fritz\",\n  \"senderName\": \"Interstellar XAF 12345\",\n  \"receiverGatewayId\": \"ng.wallet.interstellar-demo.ngn\",\n  \"receiverAmount\": \"1000\",\n  \"receiverCurrency\": \"NGN\",\n  \"receiverAccount\": \"ernest\",\n  \"receiverName\": \"Interstellar NGN 12345\",\n  \"receivingGatewayFee\": \"100\",\n  \"memo\": \"school fees\",\n  \"extras\": null,\n  \"senderGatewayTransactionId\": \"1674766017\"\n}",
  "creationRequestPublicKeyBase64": "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlHZU1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTUFEQ0JpQUtCZ0ZSYXlmRmxPKzhjYzVyUDNseERZT2JmbUViSgppTktQQWZDQ2FEemlyYyt6cU9xTFI1YkhTeklLTUMxN2lVTUpudGNGVUJZTjE1emZ4N3NVZW5qTlRPa29MVHVjCm5TRUhadzIyeFp4Uk05K0ZQTUpCaWw3bTl6cUdNZjU4Yk55UTAvTGl3cVhia3FXcHVPUzdmTnlOS2hNc2k0Q0wKdUhpMU5ncDh4YXdsM1Q1MUFnTUJBQUU9Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQ==",
  "creationRequestSignatureBase64": "RZXZdHfmQ9XGDAbxM17Gzgq3IKimJgylwvuaJpbyZ2ogzwDa5HF5GPSobaVXobvHvlO+6nPjFZgdeMBdIta1xwOZUePAZ3SGbH/Z9MkdutVUxALehuSnWAV+5YlEfV96VeHHwHjzMvcDQjsdXQYVQH7Z6fOV0zpnl9+n8ByegEQ=",
  "extras": null
}

Notice the field blockchainTransactionId. This tells us that the transaction has been recorded into the blockchain.

Notice also the field receiverGatewayStatus. It shows pending. Which means the receiving gateway is yet to process the payment.

In the next section, we will discuss how the receiving payment gateway honors a cross-border payment.