Skip to content
GitHub

Make recurring payments

The Open Payments APIs facilitate multiple use cases for recurring payments to and from Open Payments-enabled wallets. Buy Now Pay Later (BNPL) is one example, where a purchaser pays for an item in installments over regularly scheduled intervals.

This guide provides steps for making recurring payments, in which a single outgoing payment grant is used to create multiple outgoing payment resources at defined intervals.

Dependencies

Endpoints

Prerequisites

Additional configuration

Add "type": "module" to package.json

Add the following to tsconfig.json

{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022"
}
}

Import createAuthenticatedClient from the Open Payments SDK

Import dependencies

import { createAuthenticatedClient } from "@interledger/open-payments";
Copied!

Create an authenticated Open Payments client

Create an Open Payments-authenticated 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 is 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 recipient’s wallet address information

Get wallet address information

const walletAddress = await client.walletAddress.get({
  url: WALLET_ADDRESS,
});
Copied!

2. Request an Incoming Payment grant

Request an incomingPayment grant from the recipient wallet’s authorization server.

Request incoming payment grant

const grant = await client.grant.request(
  {
    url: walletAddress.authServer,
  },
  {
    access_token: {
      access: [
        {
          type: "incoming-payment",
          actions: ["list", "read", "read-all", "complete", "create"],
        },
      ],
    },
  },
);
Copied!

3. Create an Incoming Payment

Create an incomingPayment on the recipient wallet’s resource server using the access token returned by the authorization server in the grant request and specify the walletAddress with the URL of the recipient’s wallet.

Add the incomingAmount object and define the following properties:

  • value : the maximum allowable amount that will be paid to the recipient’s wallet address.
  • assetCode : the ISO 4217 currency code representing the underlying asset used to make the payment.
  • assetScale : the scale of amounts denoted in the corresponding asset code.

Optionally you may add the expiresAt property which is the date and time after which payments to the incoming payments will no longer be accepted.

Create incoming payment

const incomingPayment = await client.incomingPayment.create(
  {
    url: new URL(WALLET_ADDRESS).origin,
    accessToken: INCOMING_PAYMENT_ACCESS_TOKEN,
  },
  {
    walletAddress: WALLET_ADDRESS,
    incomingAmount: {
      value: "1000",
      assetCode: "USD",
      assetScale: 2,
    },
    expiresAt: new Date(Date.now() + 60_000 * 10).toISOString(),
  },
);
Copied!

4. Request a Quote grant

Request a quote grant from the sender wallet’s authorization server.

Request quote grant

const grant = await client.grant.request(
  {
    url: walletAddress.authServer,
  },
  {
    access_token: {
      access: [
        {
          type: "quote",
          actions: ["create", "read", "read-all"],
        },
      ],
    },
  },
);
Copied!

5. Create a Quote

Create a quote on the sender wallet’s resource server using the access token returned by the authorization server in the grant request.

Add the following properties:

  • method : the payment method used to facilitate the payment. Set this property to ilp as Open Payments only supports Interledger payments at this time.
  • walletAddress : the URL of the sender’s wallet address.
  • receiver : the URL of the incoming payment that will receive the payment.

Create quote

const quote = await client.quote.create(
  {
    url: new URL(WALLET_ADDRESS).origin,
    accessToken: QUOTE_ACCESS_TOKEN,
  },
  {
    method: "ilp",
    walletAddress: WALLET_ADDRESS,
    receiver: INCOMING_PAYMENT_URL,
  },
);
Copied!

6. Request an interactive Outgoing Payment grant

Request an outgoingPayment grant from the sender wallet’s authorization server. This request requires an interactive grant as the sender will need to consent before an outgoingPayment is made against their wallet.

Add the limits object with the one of the following properties:

  • debitAmount : the maximum amount to be deducted from the sender’s wallet.
  • receiveAmount : the maximum amount to be received in the recipient’s wallet.

Then add the interval property to the limits object, which is the interval period conforming to the ISO8601 repeating interval string format.

Next, indicate your client is capable of directing the user to a URI to start an interaction.

  1. Add the interact object to your grant request.
  2. Specify redirect as the start mode. Open Payments only supports redirect at this time. The redirect URI will be provided by the authorization server in its response.

Now indicate your client can receive a signal from the authorization server when the interaction has finished.

  1. Add the finish object within the interact object.
  2. Specify redirect as the method. Open Payments only supports redirect at this time.
  3. Specify the uri that the authorization server will send the user back to when the interaction has finished.
  4. Create a nonce for your client to use to verify the authorization server is the same entity throughout the entire process. The nonce can be any random, hard-to-guess string.

Request outgoing payment grant

const grant = await client.grant.request(
  {
    url: walletAddress.authServer,
  },
  {
    access_token: {
      access: [
        {
          identifier: walletAddress.id,
          type: "outgoing-payment",
          actions: ["list", "list-all", "read", "read-all", "create"],
          limits: {
            debitAmount: {
              assetCode: quote.debitAmount.assetCode,
              assetScale: quote.debitAmount.assetScale,
              value: quote.debitAmount.value,
            },
          },
        },
      ],
    },
    interact: {
      start: ["redirect"],
      finish: {
        method: "redirect",
        uri: "http://localhost:3344",
        nonce: NONCE,
      },
    },
  },
);
Copied!

Check grant state

if (!isPendingGrant(grant)) {
  throw new Error("Expected interactive grant");
}
Copied!

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.

9. Request a grant continuation

After the user completes their interaction with the identity provider (IdP), they should be redirected back to your application. At this point, you can make the grant continuation request. In scenarios where a user interface is not available, consider implementing a polling mechanism to check for the completion of the interaction.

Add the authorization server’s interaction reference, as the interact_ref value, to the body of your continuation request.

Continue grant

const grant = await client.grant.continue(
  {
    accessToken: CONTINUE_ACCESS_TOKEN,
    url: CONTINUE_URI,
  },
  {
    interact_ref: interactRef,
  },
);
Copied!

Issue the request to the continue uri supplied in the authorization server’s initial grant response. 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",
"read-all",
"list",
"list-all"
],
"identifier": "https://ilp.interledger-test.dev/alice",
"limits": {
"receiver": "https://ilp.interledger-test.dev/bob/incoming-payments/48884225-b393-4872-90de-1b737e2491c2",
"interval": "R12/2019-08-24T14:15:22Z/P1M",
"debitAmount": {
"value": "500",
"assetCode": "USD",
"assetScale": 2
}
}
}
]
},
"continue": {
"access_token": {
"value": "33OMUKMKSKU80UPRY5NM"
},
"uri": "https://auth.interledger-test.dev/continue/4CF492MLVMSW9MKMXKHQ",
"wait": 30
}
}

10. Create an initial Outgoing Payment

Create an outgoingPayment on the sender wallet’s resource server using the access token returned by the authorization server in the grant request.

Add the following properties:

  • walletAddress : the URL of the sender’s wallet address.
  • quoteId : the URL of the quote specifying the payment amount.

Create outgoing payment

const outgoingPayment = await client.outgoingPayment.create(
  {
    url: new URL(WALLET_ADDRESS).origin,
    accessToken: OUTGOING_PAYMENT_ACCESS_TOKEN,
  },
  {
    walletAddress: WALLET_ADDRESS,
    quoteId: QUOTE_URL,
  },
);
Copied!

11. Rotate the Access Token

Rotate the access token obtained from the previous outgoingPayment grant request.

Rotate token

const token = await client.token.rotate({
  url: MANAGE_URL,
  accessToken: ACCESS_TOKEN,
});
Copied!

12. Create another Outgoing Payment

Create another outgoingPayment on the sender wallet’s resource server using the new access token returned by the authorization server in the previous step.

Add the following properties:

  • walletAddress : the URL of the sender’s wallet address.
  • quoteId : the URL of the quote specifying the payment amount.

Create outgoing payment

const outgoingPayment = await client.outgoingPayment.create(
  {
    url: new URL(WALLET_ADDRESS).origin,
    accessToken: OUTGOING_PAYMENT_ACCESS_TOKEN,
  },
  {
    walletAddress: WALLET_ADDRESS,
    quoteId: QUOTE_URL,
  },
);
Copied!

13. Make recurring Outgoing Payments

Repeat steps 11 and 12 to facilitate as many payments as needed by your application.