Skip to content
GitHub

Make a one-time payment

The Open Payments APIs facilitate multiple use cases for one-time payments to and from Open Payments-enabled wallets. Making a payment directly from one person to another, called a peer-to-peer payment, is one example.

This guide provides the steps your Open Payments-enabled client will take to set up a one-time $10 USD payment from a sender to a recipient.

To initiate a payment, the client must get wallet address information for both the sender and recipient.

Let’s assume the sender’s wallet address is already saved in your client. The sender is essentially your client’s user. Let’s also assume the sender has entered the recipient’s wallet address into a payment form in your client application.

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 recipient’s wallet provider. A similar response will be returned from the sender’s wallet provider.

{
"id": "https://happylifebank.example.com/recipient",
"assetCode": "USD",
"assetScale": 2,
"authServer": "https://auth.happylifebank.example.com/",
"resourceServer": "https://happylifebank.example.com/op"
}

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 client to request 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
{
"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.

In the example below, the incoming payment will be for $10 USD.

const recipientIncomingPayment = await client.incomingPayment.create(
{
url: recipientWalletAddress.resourceServer,
accessToken: recipientIncomingPaymentGrant.access_token.value
},
{
walletAddress: recipientWalletAddress.id,
incomingAmount: {
value: '1000',
assetCode: 'USD',
assetScale: 2
},
},
)
Example response
{
"id": "https://happylifebank.example.com/incoming-payments/{...}",
"walletAddress": "https://happylifebank.example.com/recipient",
"incomingAmount": {
"value": "1000",
"assetCode": "USD",
"assetScale": 2
},
"receivedAmount": {
"value": "0",
"assetCode": "USD",
"assetScale": 2
},
"completed": false,
"createdAt": "2025-03-12T23:20:50.52Z",
"methods": [
{
"type": "ilp",
"ilpAddress": "...",
"sharedSecret": "..."
}
]
}

Use the sender wallet’s authServer details, received in Step 1, to call the POST Grant Request API.

This call obtains an access token that allows your client to request 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 a quote resource be created on the sender’s wallet account. The request must contain the receiver, which is the incoming payment id returned from the recipient’s wallet in Step 3.

const senderQuote = await client.quote.create(
{
url: senderWalletAddress.resourceServer,
accessToken: senderQuoteGrant.access_token.value
},
{
method: 'ilp',
walletAddress: senderWalletAddress.id,
receiver: recipientIncomingPayment.id
}
)
Example response
{
"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": "1000",
"assetCode": "USD",
"assetScale": 2
},
"receiveAmount": {
"value": "1000",
"assetCode": "USD",
"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 wallet’s authServer information, received in Step 1, to call the POST Grant Request API.

This call obtains an access token that allows your client to request 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: '1000',
}
}
}
]
},
interact: {
start: ['redirect'],
finish: {
method: 'redirect',
uri: 'https://paymentplatform.example/finish/{...}', // where the client will redirect the user to after they've completed the interaction
nonce: NONCE
}
}
}
)
Example response
{
"interact": {
"redirect": "https://auth.interledger-test.dev/{...}", // uri the client will redirect the user to, to begin the interaction
"finish": "..." // unique key to secure the callback
},
"continue": {
"access_token": {
"value": "..." // access token for continuing the outgoing payment grant request
},
"uri": "https://auth.interledger-test.dev/continue/{...}", // uri for continuing the outgoing payment grant request
"wait": 30
}
}

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.

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.

In our example, we’re assuming the IdP the user interacted with has a user interface. When the interaction completes, the user is returned to your client. Now your client 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 client to request 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",
}
]
},
"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 the quoteId.

Use the access token returned in Step 6 to call the POST Create Outgoing Payment API. Include the quoteId in the request.

const senderOutgoingPaymentToRecipient = await client.outgoingPayment.create(
{
url: senderWalletAddress.resourceServer,
accessToken: senderOutgoingPaymentGrant.access_token.value
},
{
walletAddress: senderWalletAddress.id,
quoteId: recipientQuote.id
}
)
Example response
{
"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": "1000",
"assetCode": "USD",
"assetScale": 2
},
"receiveAmount": {
"value": "1000",
"assetCode": "USD",
"assetScale": 2
},
"sentAmount": {
"value": "0",
"assetCode": "USD",
"assetScale": 2
},
"createdAt": "2022-03-12T23:20:54.52Z"
}