Send a remittance with fixed debit amount
A remittance payment is a transfer of money from one person to another, typically across borders or long distances, often involving currency conversion and fees. In this guide, you will learn how to implement a one-time remittance payment feature where your users can specify exactly how much they want to pay, rather than how much the recipient should receive.
This approach is particularly useful for remittance app scenarios where:
- Your users want to pay a fixed amount from their account
- The recipient receives whatever amount remains after fees and currency conversion
- Your users want to avoid the complexity of calculating fees upfront
Scenario
Section titled “Scenario”Imagine someone in the US sending money to a family member in Mexico. They want to send exactly $100 US Dollars (USD
) from their account, regardless of how much their family member actually receives after fees and exchange rates. This is different from sending money where the sender specifies exactly how much the recipient should receive.
For this guide, you’ll assume the role of a developer building a remittance app. The guide explains how to send a $100 USD
payment, where the sender pays exactly $100, and the recipient receives the amount in Mexican Pesos (MXN
) after all fees and currency conversion.
Example transaction details:
- Sender pays: $100.00
USD
(exact amount) - Fees: $5.00
USD
(5% processing fee) - Currency conversion:
USD
toMXN
at 17.00 exchange rate - Recipient receives: $1,615.00
MXN
(($100 - $5) × 17.00)
The three parties involved in this scenario are:
- Developer: you, the person building the remittance app
- Sender: the person using your app to send money in
USD
- Recipient: the person receiving the money in
MXN
Remember, Open Payments doesn’t execute payments or touch money in any way. It’s used to issue payment instructions before any money movement occurs. An example of a payment instruction is, “debit exactly $100 from the sender’s account and send it to the recipient’s account, where they will receive $1,615.00 MXN
after $5 in fees and USD
to MXN
conversion.”
Endpoints
Section titled “Endpoints”- GET Get Wallet Address
- POST Grant Request
- POST Create Incoming Payment
- POST Create a Quote
- POST Create an Outgoing Payment
1. Get wallet address information
Section titled “1. Get wallet address information”When your user initiates a payment through your remittance app, you need to get wallet address information for both the sender and the recipient.
Let’s assume your user has already provided their own wallet address when they signed up to use your app. Let’s also assume the sender entered the recipient’s wallet address into your app’s payment form when initiating the payment.
Call the GET Get Wallet Address API for each address.
const senderWalletAddress = await client.walletAddress.get({ url: 'https://cloudninebank.example.com/sender'})const recipientWalletAddress = await client.walletAddress.get({ url: 'https://happylifebank.example.com/recipient'})
Example response
The following shows an example response from the sender’s wallet provider. A similar response will be returned from the recipient’s wallet provider.
{ "id": "https://cloudninebank.example.com/sender", "assetCode": "USD", "assetScale": 2, "authServer": "https://auth.cloudninebank.example.com/", "resourceServer": "https://cloudninebank.example.com/op"}
2. Request an incoming payment grant
Section titled “2. Request an incoming payment grant”Use the recipient’s authServer
details, received in the previous step, to call the POST Grant Request API.
This call obtains an access token that allows your app to request that an incoming payment resource be created on the recipient’s wallet account.
const recipientIncomingPaymentGrant = await client.grant.request( { url: recipientWalletAddress.authServer }, { access_token: { access: [ { type: "incoming-payment", actions: ["read", "create"], }, ], }, },);
Example response
The following shows an example response from the recipient’s wallet provider.
{ "access_token": { "value": "...", // access token value for incoming payment grant "manage": "https://happylifebank.example.com/token/{...}", // management uri for access token "access": [ { "type": "incoming-payment", "actions": ["create", "read"] } ] }, "continue": { "access_token": { "value": "..." // access token for continuing the request }, "uri": "https://happylifebank.example.com/continue/{...}" // continuation request uri }}
3. Request the creation of an incoming payment resource
Section titled “3. Request the creation of an incoming payment resource”Use the access token returned in the previous response to call the POST Create Incoming Payment API.
This call requests an incoming payment resource be created on the recipient’s wallet account.
const recipientIncomingPayment = await client.incomingPayment.create( { url: recipientWalletAddress.resourceServer, accessToken: recipientIncomingPaymentGrant.access_token.value }, { walletAddress: recipientWalletAddress.id },)
Example response
The following shows an example response from the recipient’s wallet provider.
{ "id": "https://happylifebank.example.com/incoming-payments/{...}", "walletAddress": "https://happylifebank.example.com/recipient", "receivedAmount": { "value": "0", "assetCode": "USD", "assetScale": 2 }, "completed": false, "createdAt": "2025-03-12T23:20:50.52Z", "methods": [ { "type": "ilp", "ilpAddress": "...", "sharedSecret": "..." } ]}
4. Request a quote grant
Section titled “4. Request a quote grant”Use the sender’s authServer
details, received in Step 1, to call the POST Grant Request API.
This call obtains an access token that allows your app to request that a quote resource be created on the sender’s wallet account.
const senderQuoteGrant = await client.grant.request( { url: senderWalletAddress.authServer }, { access_token: { access: [ { type: 'quote', actions: ['create', 'read'] } ] } })
Example response
{ "access_token": { "value": "...", // access token value for quote grant "manage": "https://cloudninebank.example.com/token/{...}", // management uri for access token "access": [ { "type": "quote", "actions": ["create", "read"] } ] }, "continue": { "access_token": { "value": "..." // access token for continuing the request }, "uri": "https://auth.cloudninebank.example.com/continue/{...}" // continuation request uri }}
5. Request the creation of a quote resource
Section titled “5. Request the creation of a quote resource”Use the access token, received in the previous step, to call the POST Create Quote API.
This call requests that a quote resource be created on the sender’s wallet account. The request must contain the receiver
, which is the recipient’s incoming payment id
, along with the debitAmount
, which is the exact amount the sender wants to pay.
The debitAmount
specifies that the sender will pay exactly $100 USD, and the recipient will receive whatever amount remains after all fees and currency conversion.
const senderQuote = await client.quote.create( { url: senderWalletAddress.resourceServer, accessToken: senderQuoteGrant.access_token.value }, { method: 'ilp', walletAddress: senderWalletAddress.id, receiver: recipientIncomingPayment.id, debitAmount: { value: '10000', // $100.00 USD assetCode: 'USD', assetScale: 2 } })
The response returns a receiveAmount
, a debitAmount
, and other required information.
debitAmount
- The amount the sender must pay (exactly $100.00 USD in our example).receiveAmount
- The amount the recipient will actually receive ($1,650.00 MXN in our example) after fees and currency conversion.
Example response
The following shows an example response from the sender’s wallet provider.
{ "id": "https://cloudninebank.example.com/quotes/{...}", // url identifying the quote "walletAddress": "https://cloudninebank.example.com/sender", "receiver": "https://happylifebank.example.com/incoming-payments/{...}", // url of the incoming payment the quote is created for "debitAmount": { "value": "10000", // Sender pays exactly $100.00 USD "assetCode": "USD", "assetScale": 2 }, "receiveAmount": { "value": "161500", // Recipient receives 1,615.00 MXN after fees and currency conversion "assetCode": "MXN", "assetScale": 2 }, "method": "ilp", "createdAt": "2025-03-12T23:22:51.50Z"}
6. Request an interactive outgoing payment grant
Section titled “6. Request an interactive outgoing payment grant”Use the sender’s authServer
information received in Step 1 to call the POST Grant Request API.
This call obtains an access token that allows your app to request that an outgoing payment resource be created on the sender’s wallet account.
const pendingSenderOutgoingPaymentGrant = await client.grant.request( { url: senderWalletAddress.authServer }, { access_token: { access: [ { identifier: senderWalletAddress.id, type: 'outgoing-payment', actions: ['read', 'create'], limits: { debitAmount: { assetCode: 'USD', assetScale: 2, value: quote.debitAmount.value, // 10000 } } } ] }, interact: { start: ['redirect'], finish: { method: 'redirect', uri: 'https://myapp.example.com/finish/{...}', // where to redirect your user after they've completed the interaction nonce: NONCE } } })
Example response
{ "interact": { "redirect": "https://auth.cloudninebank.example.com/{...}", // uri to redirect the sender to, to begin interaction "finish": "..." // unique key to secure the callback }, "continue": { "access_token": { "value": "..." // access token for continuing the outgoing payment grant request }, "uri": "https://auth.cloudninebank.example.com/continue/{...}", // uri for continuing the outgoing payment grant request "wait": 30 }}
7. Start interaction with the user
Section titled “7. Start interaction with the user”Once the client receives the authorization server’s response, it must send the user to the interact.redirect
URI contained in the response. This starts the interaction flow.
The response also includes a continue
object, which is essential for managing the interaction and obtaining explicit user consent for outgoing payment grants. The continue
object contains an access token and a URI that the client will use to finalize the grant request after the user has completed their interaction with the identity provider (IdP). This ensures that the client can securely obtain the necessary permissions to proceed with the payment process.
8. Finish interaction with the user
Section titled “8. Finish interaction with the user”The user interacts with the authorization server through the server’s interface and approves or denies the grant.
Provided the user approves the grant, the authorization server:
- Sends the user to the
finish.uri
provided in the interactive outgoing payment grant request. The means by which the server sends the user to the URI is out of scope, but common options include redirecting the user from a web page and launching the system browser with the target URI. - Secures the redirect by adding a unique hash, allowing your client to validate the
finish
call, and an interaction reference as query parameters to the URI.
9. Request a grant continuation
Section titled “9. Request a grant continuation”In our example, we’re assuming the IdP your user interacted with has a user interface. When the interaction completes, your user returns to your app. Now your app can make a continuation request for the outgoing payment grant.
Call the POST Grant Continuation Request API. This call requests an access token that allows your app to request that an outgoing payment resource be created on the sender’s wallet account.
Issue the request to the continue.uri
provided in the initial outgoing payment grant response (Step 6).
Include the interact_ref
returned in the redirect URI’s query parameters.
const senderOutgoingPaymentGrant = await client.grant.continue( { url: pendingSenderOutgoingPaymentGrant.continue.uri, accessToken: pendingSenderOutgoingPaymentGrant.continue.access_token.value }, { interact_ref: interactRef })
Example response
{ "access_token": { "value": "...", // final access token required before creating outgoing payments "manage": "https://auth.cloudninebank.example.com/token/{...}", // management uri for access token "access": [ { "type": "outgoing-payment", "actions": ["create", "read"], "identifier": "https://cloudninebank.example.com/sender", "limits": { "receiver": "https://happylifebank.example.com/incoming-payments/{...}" // url of the incoming payment that's being paid } } ] }, "continue": { "access_token": { "value": "..." // access token for continuing the request }, "uri": "https://auth.cloudninebank.example.com/continue/{...}" // continuation request uri }}
10. Request the creation of an outgoing payment resource
Section titled “10. Request the creation of an outgoing payment resource”Recall that the Create Quote API response (Step 5) included a debitAmount
and a receiveAmount
. The response also included an id
which is a URL to identify the quote.
Because the quote contains debit and receive amounts, we won’t specify any other amounts when setting up the outgoing payment. Instead, we will specify a quoteId
.
Use the access token returned in Step 9 to call the POST Create Outgoing Payment API. Include the quoteId
in the request.
const senderOutgoingPayment = await client.outgoingPayment.create( { url: senderWalletAddress.resourceServer, accessToken: senderOutgoingPaymentGrant.access_token.value }, { walletAddress: senderWalletAddress.id, quoteId: senderQuote.id })
Example response
The following shows an example response when an outgoing payment resource is created on the sender’s account.
{ "id": "https://cloudninebank.example.com/outgoing-payments/{...}", // url identifying the outgoing payment "walletAddress": "https://cloudninebank.example.com/sender", "receiver": "https://happylifebank.example.com/incoming-payments/{...}", // url of the incoming payment being paid "debitAmount": { "value": "10000", // Sender pays exactly $100.00 USD "assetCode": "USD", "assetScale": 2 }, "receiveAmount": { "value": "161500", // Recipient receives 1,615.00 MXN after fees and currency conversion "assetCode": "MXN", "assetScale": 2 }, "sentAmount": { "value": "0", "assetCode": "USD", "assetScale": 2 }, "createdAt": "2022-03-12T23:20:54.52Z"}