Skip to content
GitHub

Accept a one-time payment for an online purchase

In a business-to-consumer (B2C) model, businesses sell their products directly to consumers, bypassing any intermediaries. This model is also referred to as direct-to-consumer (D2C).

Online retailers often employ the B2C model, allowing customers to pay directly for goods and services. In cases where an intermediary or third-party is owed a portion of the sale, the payment can be split.

For this guide, you’ll assume the role of a developer working for an online athletics company. A customer adds a pair of shoes to their shopping cart and begins the checkout process. Their total is $1,400 MXN. This guide explains how you can implement Open Payments on the retailer’s site so that the retailer receives the full amount of the customer’s payment.

The parties involved in the transaction are the:

  • Retailer: the athletics company
  • Developer: you, as the developer working on the client app
  • Client app: The retailer’s website
  • Customer: the individual using the retailer’s website to make a purchase

When the customer initiates the payment, the client app (the retailer’s site) must get wallet address information for both the customer and themselves.

Let’s assume the customer entered their wallet address into the site’s checkout form. Let’s also assume that the retailer’s wallet address is coded into the checkout form.

Call the GET Get Wallet Address API for each address.

const customerWalletAddress = await client.walletAddress.get({
url: 'https://cloudninebank.example.com/customer'
})
const retailerWalletAddress = await client.walletAddress.get({
url: 'https://happylifebank.example.com/retailer'
})

Example response The following is an example response from the customer’s wallet provider. A similar response will be returned from the retailer’s wallet provider.

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

Use the retailer’s authServer details, received in the previous step, to call the POST Grant Request API.

This call obtains an access token that allows the client app to request an incoming payment resource be created on the retailer’s wallet account.

const retailerIncomingPaymentGrant = await client.grant.request(
{
url: retailerWalletAddress.authServer
},
{
access_token: {
access: [
{
type: 'incoming-payment',
actions: ['create'],
},
],
},
},
);

Example response The following is an example response from the retailer’s wallet provider.

{
"access_token": {
"value": "...", // access token value for incoming payment grant
"manage": "https://auth.happylifebank.example.com/token/{...}", // management uri for access token
"access": [
{
"type": "incoming-payment",
"actions": ["create"]
}
]
},
"continue": {
"access_token": {
"value": "..." // access token for continuing the request
},
"uri": "https://auth.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 an Incoming Payment API.

This call requests an incoming payment resource be created on the retailer’s wallet account.

Remember that the full amount of the customer’s purchase is $1,400 MXN.

const retailerIncomingPayment = await.client.incomingPayment.create(
{
url: retailerWalletAddress.resourceServer,
accessToken: retailerIncomingPaymentGrant.access_token.value
},
{
walletAddress: retailerWalletAddress.id,
incomingAmount: {
value: '140000',
assetCode: 'MXN',
assetScale: 2
},
},
)

Example response The following is an example response from the retailer’s wallet provider.

{
"id": "https://happylifebank.example.com/incoming-payments/{...}",
"walletAddress": "https://happylifebank.example.com/retailer",
"incomingAmount": {
"value": "140000",
"assetCode": "MXN",
"assetScale": 2
},
"receivedAmount": {
"value": "0",
"assetCode": "MXN",
"assetScale": 2
},
"completed": false,
"createdAt": "2025-03-12T23:20:50.52Z",
"methods": [
{
"type": "ilp",
"ilpAddress": "...",
"sharedSecret": "..."
}
]
}

Use the customer’s authServer details, returned in Step 1, to call the POST Grant Request API.

This call obtains an access token that allows the client app to request a quote resource be created on the customer’s wallet account.

const customerQuoteGrant = await client.grant.request(
{
url: customerWalletAddress.authServer
},
{
access_token: {
access: [
{
type: 'quote',
actions: ['create']
}
]
}
}
)

Example response The following is an example response from the customer’s wallet provider.

{
"access_token": {
"value": "...", // access token value for quote grant
"manage": "https:/auth.cloudninebank.example.com/token/{...}", // management uri for access token
"access": [
{
"type": "quote",
"actions": ["create"]
}
]
},
"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 customer’s wallet account.

The request must contain the receiver, which is the id of the incoming payment. The id was returned in the Create an Incoming Payment API response in Step 3.

const customerQuote = await client.quote.create(
{
url: customerWalletAddress.resourceServer,
accessToken: customerQuoteGrant.access_token.value
},
{
method: 'ilp',
walletAddress: customerWalletAddress.id,
receiver: retailerIncomingPayment.id
}
)

The response returns a receiveAmount, a debitAmount, and other required information.

  • debitAmount - The amount (in MXN) that will be charged to the customer
  • receiveAmount - The incomingAmount value from the incoming payment resource

Example response The following is an example response from the customer’s wallet provider.

{
"id": "https://cloudninebank.example.com/quotes/{...}", // url identifying the quote
"walletAddress": "https://cloudninebank.example.com/customer",
"receiver": "https://happylifebank.example.com/incoming-payments/{...}", // url of the incoming payment the quote is created for
"debitAmount": {
"value": "140000",
"assetCode": "MXN",
"assetScale": 2
},
"receiveAmount": {
"value": "140000",
"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 customer’s authServer information to call the POST Grant Request API.

This call obtains an access token that allows the client app to request outgoing payment resources be created on the customer’s wallet account. For this guide, the request will be limited up to the amount of 140000 ($1400.00)

const pendingCustomerOutgoingPaymentGrant = await client.grant.request(
{
url: customerWalletAddress.authServer
},
{
access_token: {
access: [
{
identifier: customerWalletAddress.id,
type: 'outgoing-payment',
actions: ['create'],
limits: {
debitAmount: {
assetCode: 'MXN',
assetScale: 2,
value: '140000',
}
}
}
]
},
interact: {
start: ['redirect'],
finish: {
method: 'redirect',
uri: 'https://paymentplatform.example/finish/{...}', // where to redirect the customer after they've completed the interaction
nonce: NONCE
}
}
}
)

Example response The following is an example response from the customer’s wallet provider.

{
"interact": {
"redirect": "https://auth.interledger-test.dev/{...}", // uri to redirect the customer 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.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 customer interacted with has a user interface. When the interaction completes, the customer returns to the client app. Now the 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 the client app to request an outgoing payment resource be created on the customer’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 customerOutgoingPaymentGrant = await client.grant.continue(
{
url: pendingCustomerOutgoingPaymentGrant.continue.uri,
accessToken: pendingCustomerOutgoingPaymentGrant.continue.access_token.value
},
{
interact_ref: interactRef
}
)

Example response The following is an example response from the customer’s wallet provider.

{
"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"],
"identifier": "https://cloudninebank.example.com/customer",
"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”

Use the access token returned in Step 9 to call the POST Create Outgoing Payment API. This call requests the creation of an outgoing payment resource be created on the customer’s wallet account. Include the quoteId in the request. The quoteId is the id returned in the Create Quote API response (Step 5).

const customerOutgoingPayment = await client.outgoingPayment.create(
{
url: customerWalletAddress.resourceServer,
accessToken: customerOutgoingPaymentGrant.access_token.value
},
{
walletAddress: customerWalletAddress.id,
quoteId: customerQuote.id
}
)

Example response The following is an example response from the customer’s wallet provider.

{
"id": "https://cloudninebank.example.com/outgoing-payments/{...}", // url identifying the outgoing payment
"walletAddress": "https://cloudninebank.example.com/customer",
"receiver": "https://happylifebank.example.com/incoming-payments/{...}", // url of the incoming payment being paid
"debitAmount": {
"value": "140000",
"assetCode": "MXN",
"assetScale": 2
},
"receiveAmount": {
"value": "140000",
"assetCode": "MXN",
"assetScale": 2
},
"sentAmount": {
"value": "0",
"assetCode": "MXN",
"assetScale": 2
},
"createdAt": "2022-03-12T23:20:54.52Z"
}