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.
- First you create a
payment-intent
. PAPE will return rates, fees, and name verification checks. - 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
field | type | description |
---|---|---|
senderGatewayId | string | Payment Gateway ID of the sender |
senderCurrency | string | Currency used by the sender |
senderAccount | string | Sender account |
receiverGatewayId | string | Payment Gateway ID of the receiver |
receiverAccount | string | Receiver account |
receiverCurrency | string | Currency used by the receiver |
receiverAmount | string | Amount in receiver currency the destination will receive |
memo | string | memo associated with the payment |
Payment
field | type | description |
---|---|---|
senderGatewayId | string | Payment Gateway ID of the sender |
senderCurrency | string | Currency used by the sender |
senderAccount | string | Sender account |
receiverGatewayId | string | Payment Gateway ID of the receiver |
receiverAccount | string | Receiver account |
receiverCurrency | string | Currency used by the receiver |
receiverAmount | string | Amount in receiver currency the destination will receive |
memo | string | memo associated with the payment |
senderAmount | string | Amount sender has to pay, excluding sender fees |
senderName | string | Name of the sender |
receiverName | string | Name of the receiver |
receivingGatewayFee | string | Amount in receiver currency charged by the receiving gateway |
senderGatewayTransactionId | string | Transaction id generated by the sending gateway when sending a payment |
receiverGatewayTransactionId | string | Transaction id generated by the receiving gateway when honoring a payment |
senderGatewayStatus | string | Status of the payment according to the sending gateway |
receiverGatewayStatus | string | Status of the payment according to the receiving gateway |
blockchainTransactionId | string | Blockchain transaction ID of the payment |
API Calls
Create a new unsigned payment intent.
{
"senderGatewayId"
: “[string]
Payment Gateway ID of the sender.”,
"senderCurrency"
: “[string]
Currency used by the sender.”,
"senderAccount"
: “[string]
Sender Account.”,
"receiverGatewayId"
: “[string]
Payment Gateway ID of the receiver.”,
"receiverAccount"
: “[string]
Receiver account.”,
"receiverCurrency"
: “[string]
Currency used by the receiver.”,
"receiverAmount"
: “[string]
Amount in receiver currency the destination will receive.”,
"memo"
: “[string]
memo associated with the payment.”,
}
Endpoints
staging
: https://apis-alpha.dev.projectwhite.io
Responses
http code | content-type | response |
---|---|---|
200 | application/json | Payment Intent Object. (see data model above) |
4xx | application/json | {"error": "string","message": "string" |
Creates a new signed payment.
{
"senderGatewayId"
: “[string]
Payment Gateway ID of the sender.”,
"senderCurrency"
: “[string]
Currency used by the sender.”,
"senderAccount"
: “[string]
Sender Account.”,
"receiverGatewayId"
: “[string]
Payment Gateway ID of the receiver.”,
"receiverAccount"
: “[string]
Receiver account.”,
"receiverCurrency"
: “[string]
Currency used by the receiver.”,
"receiverAmount"
: “[string]
Amount in receiver currency the destination will receive.”,
"memo"
: “[string]
Memo associated with the payment.”,
"senderAmount"
: “[string]
Amount sender has to pay, excluding sender fees.”,
"senderName"
: “[string]
Name of the sender.”,
"receiverName"
: “[string]
Name of the receiver.”,
"receivingGatewayFee"
: “[string]
Amount in receiver currency charged by the receiving gateway.”,
"senderGatewayTransactionId"
: “[string]
Transaction id generated by the sending gateway when sending a payment.”,
}
Endpoints
staging
: https://apis-alpha.dev.projectwhite.io
Responses
http code | content-type | response |
---|---|---|
200 | application/json | Payment Intent Object. (see data model above) |
4xx | application/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
ormac
: 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
- mac:
jq: To process json on the command line.
- mac:
brew install jq
- ubuntu/debian:
sudo apt-get install jq
- mac:
jo: To create json on the command line.
- mac:
brew install jo
- ubuntu/debian:
sudo apt-get install jo
- mac:
openssl: To perform cryptographic operations on the command line.
- mac:
brew install openssl
- ubuntu/debian:
sudo apt-get install openssl
- mac:
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.