Split an incoming payment
In the context of Open Payments, a split payment is a single payment that’s divided into two or more smaller payments before reaching the recipients. From the sender’s point of view, they’re making a single payment to a single recipient.
In this guide you will assume the role of a platform operator adding support for split payments to your online marketplace. When a consumer makes a purchase on your marketplace, both you and the merchant will receive a portion of the payment.
The three parties involved in the transaction are the:
- Customer: the purchaser of a good or service on the marketplace
- Merchant: the seller of a good or service on the marketplace
- Platform: you, as the operator of the marketplace
Endpoints
-
Get a Wallet address
-
Grant Request
-
Create an Incoming Payment
-
Create a Quote
-
Create an Outgoing Payment
Prerequisites
- Node 20 or higher
- A package manager such as NPM or PNPM
-
Open Payments SDK
- TSX Additional configuration
- Add
"type": "module"
topackage.json
. - Add the following to
tsconfig.json
{"compilerOptions": {"target": "ES2022","module": "ES2022"}} - Import
createAuthenticatedClient
from the Open Payments SDK.Import dependencies
Copied!import { createAuthenticatedClient } from "@interledger/open-payments";
- Create an authenticated Open Payments client by providing the following properties:
walletAddressURL
: your Open Payments-enabled wallet address that your client will use to authenticate itself to one or more authorization servers.privateKey
: the EdDSA-Ed25519 key or preferably the absolute or relative file path to the key that’s bound to your wallet address. A public key signed with this private key must be made available as a public JWK document at{walletAddressUrl}/jwks.json
url.keyId
: the identifier of the private key and the corresponding public key.Initialize Open Payments client
Copied!const client = await createAuthenticatedClient({ walletAddressUrl: WALLET_ADDRESS, privateKey: PRIVATE_KEY_PATH, keyId: KEY_ID, });
Steps
1. Get wallet address information
For the customer to initiate a payment, your website must get wallet address information for the customer, the merchant, and your marketplace platform. Let’s assume that your wallet address is built in to your platform and the merchant saved their wallet address in their marketplace account. Let’s also assume the customer provided their wallet address during the checkout flow.
In this example, we assume each party has an Open Payments-enabled account with a different account servicing entity.
const customerWalletAddress = await client.walletAddress.get({ url: 'https://cloudninebank.example.com/customer'})
const merchantWalletAddress = await client.walletAddress.get({ url: 'https://happylifebank.example.com/merchant'})
const platformWalletAddress = await client.walletAddress.get({ url: 'https://coolwallet.example.com/platform'})
2. Request Incoming Payment grants
Using the authorization server information received from the previous step, request two incomingPayment
grants: one from the merchant wallet authorization server and one from your platform wallet authorization server. The grants will allow an incomingPayment
resource to be created on both your and your merchant’s Open Payments accounts.
const merchantGrant = await client.grant.request( { url: merchantWalletAddress.authServer, }, { access_token: { access: [ { type: "incoming-payment", actions: ["read", "create"], }, ], }, },);
const platformGrant = await client.grant.request( { url: platformWalletAddress.authServer, }, { access_token: { access: [ { type: "incoming-payment", actions: ["read", "create"], }, ], }, },);
3. Create Incoming Payments
Using the access tokens provided by the authorization servers in the previous step, request the creation of two incomingPayment
resources: one on the merchant’s wallet account and one on your platform wallet account.
In this example, you will give the merchant 99% of the payment while retaining 1% as a platform operation fee.
const merchantIncomingPayment = await client.incomingPayment.create( { url: new URL(WALLET_ADDRESS).origin, accessToken: INCOMING_PAYMENT_ACCESS_TOKEN }, { walletAddress: MERCHANT_WALLET_ADDRESS, incomingAmount: { value: '9900', assetCode: 'USD', assetScale: 2 }, expiresAt: new Date(Date.now() + 60_000 * 10).toISOString() })
const platformIncomingPayment = await client.incomingPayment.create( { url: new URL(WALLET_ADDRESS).origin, accessToken: INCOMING_PAYMENT_ACCESS_TOKEN }, { walletAddress: PLATFORM_WALLET_ADDRESS, incomingAmount: { value: '100', assetCode: 'USD', assetScale: 2 }, expiresAt: new Date(Date.now() + 60_000 * 10).toISOString() })
4. Request a Quote grant
Request a quote
grant from the customer wallet authorization server. You received the authorization server URL as part of Step 1.
const customerGrant = await client.grant.request( { url: customerWalletAddress.authServer }, { access_token: { access: [ { type: 'quote', actions: ['create', 'read'] } ] } })
5. Create Quotes
Using the customer-side access token received from the previous step, request the creation of two quote
resources: one on the merchant wallet account and one on your platform wallet account.
Each request should contain its respective INCOMING_PAYMENT_URL
received from Step 3.
const merchantQuote = await client.quote.create( { url: new URL(WALLET_ADDRESS).origin, accessToken: QUOTE_ACCESS_TOKEN }, { method: 'ilp', walletAddress: WALLET_ADDRESS, receiver: INCOMING_PAYMENT_URL })
const platformQuote = await client.quote.create( { url: new URL(WALLET_ADDRESS).origin, accessToken: QUOTE_ACCESS_TOKEN }, { method: 'ilp', walletAddress: WALLET_ADDRESS, receiver: INCOMING_PAYMENT_URL })
6. Request an interactive Outgoing Payment grant for the customer
Request an interactive outgoingPayment
grant from the customer wallet authorization server.
Include the receiveAmount
object within the limits
object. The value
property must be the total amount the customer has agreed to pay.
const customerGrant = await client.grant.request( { url: customerWalletAddress.authServer }, { access_token: { access: [ { identifier: customerWalletAddress.id, type: 'outgoing-payment', actions: ['read', 'create'], limits: { receiveAmount: { assetCode: 'USD', assetScale: 2, value: '10000' } } } ] }, interact: { start: ['redirect'], finish: { method: 'redirect', uri: 'http://localhost:3344', nonce: NONCE } } })
7. Start interaction with the user
Once your 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.
{ "interact": { "redirect": "https://auth.interledger-test.dev/4CF492MLVMSW9MKMXKHQ", "finish": "4105340a-05eb-4290-8739-f9e2b463bfa7" }, "continue": { "access_token": { "value": "33OMUKMKSKU80UPRY5NM" }, "uri": "https://auth.interledger-test.dev/continue/4CF492MLVMSW9MKMXKHQ", "wait": 30 }}
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 your client’s previously defined
finish.uri
. 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.
You’ll need the interaction reference (interact_ref
) in the next step.
9. Request a grant continuation
In this guide, we’re assuming the IdP has a user interface with which the customer interacts. When the interaction completes, the customer should be directed back to your platform. Now you can make the grant continuation request.
Add the interact_ref
returned by the client wallet authorization server in the previous step.
const customerGrant = await client.grant.continue( { accessToken: CONTINUE_ACCESS_TOKEN, url: CONTINUE_URI }, { interact_ref: interactRef })
Issue the request to the continue uri
supplied in the authorization server’s initial grant response (Step 6). For example:
POST https://auth.interledger-test.dev/continue/4CF492MLVMSW9MKMXKHQ
A successful continuation response from the authorization server provides your client with an access token and other information necessary to continue the transaction.
{ "access_token": { "value": "OS9M2PMHKUR64TB8N6BW7OZB8CDFONP219RP1LT0", "manage": "https://auth.interledger-test.dev/token/dd17a202-9982-4ed9-ae31-564947fb6379", "expires_in": 3600, "access": [ { "type": "outgoing-payment", "actions": ["create", "read"], "identifier": "customerWalletAddress.id", "limits": { "receiver": "INCOMING_PAYMENT_URL", "debitAmount": { "value": "10000", "assetCode": "USD", "assetScale": 2 } } } ] }, "continue": { "access_token": { "value": "33OMUKMKSKU80UPRY5NM" }, "uri": "https://auth.interledger-test.dev/continue/4CF492MLVMSW9MKMXKHQ", "wait": 30 }}
10. Create outgoing payments
Using the merchant and platform access tokens provided in the quote responses in Step 5, request the creation of two outgoingPayment
resources on the customer’s wallet account: one for the merchant and one for your platform.
The quoteId
is the URL of the quote that specified the amount to be paid to the merchant and the platform, respectively.
const customerOutgoingPayment = await client.outgoingPayment.create( { url: customerWalletAddress.resourceServer, accessToken: OUTGOING_PAYMENT_ACCESS_TOKEN }, { walletAddress: customerWalletAddress.id, quoteId: merchantQuote.id })
const customerOutgoingPayment = await client.outgoingPayment.create( { url: new URL(WALLET_ADDRESS).origin, accessToken: OUTGOING_PAYMENT_ACCESS_TOKEN }, { walletAddress: customerWalletAddress.id, quoteId: platformQuote.id })