Skip to content
GitHub

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

Prerequisites
  • Node 20 or higher
  • A package manager such as NPM or PNPM
  • Open Payments SDK

  • TSX

    Additional configuration
  1. Add "type": "module" to package.json.
  2. Add the following to tsconfig.json
    {
    "compilerOptions": {
    "target": "ES2022",
    "module": "ES2022"
    }
    }
  3. Import createAuthenticatedClient from the Open Payments SDK.

    Import dependencies

    import { createAuthenticatedClient } from "@interledger/open-payments";
    Copied!
  4. 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

      const client = await createAuthenticatedClient({
        walletAddressUrl: WALLET_ADDRESS,
        privateKey: PRIVATE_KEY_PATH,
        keyId: KEY_ID,
      });
      Copied!

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"],
},
],
},
},
);

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()
}
)

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
}
)

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.

Example response
{
"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.

Example response
{
"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
}
)