This is the full developer documentation for Rafiki
# Hello from Rafiki
> Rafiki is open source software that provides an efficient solution for an account servicing entity (ASE) to enable Interledger functionality on its users' accounts.
[Try it out!](/integration/playground/overview)
[Test Rafiki by running two mock ASEs that automatically peer with one another.](/integration/playground/overview)
[Integration requirements](/integration/requirements/overview)
[Review the requirements for deploying Rafiki to a production environment.](/integration/requirements/overview)
[View Backend API schema](/apis/graphql/backend)
[Discover what’s in our Backend GraphQL schema.](/apis/graphql/backend)
[View Auth API schema](/apis/graphql/auth)
[Discover what’s in our Auth GraphQL schema.](/apis/graphql/auth)
# 404 - Page Not Found
> It seems like the page you're looking for doesn't exist.
# Rafiki Admin application user guide
The Rafiki Admin app provides tools to manage:
* Tenants
* Assets
* Peers
* Wallet addresses
* Webhook events
* Payments
* Account settings
It functions as an interface to the [Rafiki Admin APIs](/apis/graphql/admin-api-overview) and all actions performed, such as fetching data or executing commands, are passed to the Rafiki `backend` service. The purpose of this document is to help you navigate and use the Rafiki Admin app.
## [Getting started](#getting-started)
[Section titled “Getting started”](#getting-started)
### [Prerequisites](#prerequisites)
[Section titled “Prerequisites”](#prerequisites)
* Familiarity with general Rafiki concepts. The [Rafiki overview](/overview/overview) is a great place to start.
* Running the Rafiki `frontend` package. See [Frontend service](/integration/deployment/services/frontend-service) for more information.
### [API credential configuration](#api-credential-configuration)
[Section titled “API credential configuration”](#api-credential-configuration)
To access your data and manage resources in your tenant, you must configure your API credentials. Enter your tenant ID and API secret and select **Save Credentials**.
The Rafiki Admin app HMAC-signs requests (HMAC SHA-256) to the Backend Admin API and includes a `tenant-id` header. Requests are scoped to the tenant whose credentials you provide.
[View full image](/img/admin-guide/home-pre.png)
After successfully configuring your API credentials, a message will appear indicating that the credentials have been configured, and the left navigation menu will be populated with the available options.
[View full image](/img/admin-guide/home-post.png)
The **Clear Credentials** button allows you to clear your stored API credentials. You must clear your credentials to switch tenants. Requests will then be scoped to the new tenant.
## [Identity and user management](#identity-and-user-management)
[Section titled “Identity and user management”](#identity-and-user-management)
Rafiki Admin relies on the [Ory Kratos](https://www.ory.sh/docs/kratos/ory-kratos-intro) identity and user management solution to handle authentication (login) and user management (account creation and password recovery).
Note
Ory Kratos and Rafiki Admin should be hosted on the same top-level domain. Hosting Kratos on a subdomain is not recommended by Ory, but if you choose this approach, ensure you follow the guidelines provided in the [Kratos documentation](https://www.ory.sh/docs/kratos/debug/csrf#running-on-separate-subdomains).
### [Login and account management](#login-and-account-management)
[Section titled “Login and account management”](#login-and-account-management)
Access to Rafiki Admin uses an invitation-only system to ensure that only authorized users can register for an account. New users must be invited by an administrator. The registration flow isn’t public, so users can’t sign up on their own. Instead, administrators create accounts using the `invite-user` script.
#### [Invite a user](#invite-a-user)
[Section titled “Invite a user”](#invite-a-user)
An administrator (someone with backend interface system access) can run the `invite-user` script in one of two ways: from outside the container on the host machine where Docker is running or directly inside the Rafiki Admin Docker container.
Outside container on host machine
```nginx
docker exec -it npm run invite-user -- example@mail.com
```
Inside Rafiki Admin Docker container
```nginx
npm run invite-user -- example@mail.com
```
After running the `invite-user` script, the script generates a recovery link that also serves as an invitation link. This link is output to the terminal, and the administrator can send it to the user. When the user opens the link in their browser, they’re automatically logged in and taken to the account settings page where they can set a new password. Afterward, they can log in normally via the Rafiki Admin URL.
[View full image](/img/admin-guide/account-settings-recovery.png)
Note
The invitation link is single-use for security purposes. Once accessed, it becomes invalid.
If sending the link through Slack, ensure you format it as code by placing it inside backticks (\`) to prevent Slack from automatically previewing the link, which would invalidate it. For example:
```js
`http://localhost:4433/self-service/recovery?flow=116250ee-07bd-4b5c-a98e-87406192bb4b&token=miv0yZ7DFKKw8RyBBQvWoOsTRa2TVuZm`
```
#### [Generate a recovery link](#generate-a-recovery-link)
[Section titled “Generate a recovery link”](#generate-a-recovery-link)
Rafiki Admin provides an automated account recovery flow which requires an SMTP mail server for sending recovery links to users. Alternatively, an administrator can generate a recovery link using the same `invite-user` script.
#### [Remove a user](#remove-a-user)
[Section titled “Remove a user”](#remove-a-user)
To remove a user, administrators can run the following script in a terminal window:
```nginx
docker exec -it npm run delete-user -- example@mail.com.
```
### [Why Ory Kratos?](#why-ory-kratos)
[Section titled “Why Ory Kratos?”](#why-ory-kratos)
We chose Kratos for its open-source nature, lightweight design, and robust security features. It eliminates the need to manage password hashing, storage, or account recovery flows ourselves, allowing us to focus on what we do best.
Kratos also enhances security with features like built-in breach detection, secure session management, and regular security updates.
Ory Kratos provides frontend components (such as forms and buttons) for identity management flows like login, and account settings. These components are not fixed in design; they’re fetched via API calls which allows us to match the identity management components with Rafiki Admin’s overall look and feel.
Disabling authentication
Rafiki Admin provides access to sensitive data like peering relationships and wallet addresses. Authentication is enabled by default to restrict access to authorized users only.
In the [Local Playground](/integration/playground/overview#rafiki-admin), authentication is disabled by default to simplify local development and testing. To disable it in other environments, set the environment variable `AUTH_ENABLED` to `false`. This should be done with **extreme caution** and only in specific scenarios:
* Secure, non-production environments like the Local Playground for local development and testing.
* Internal systems where Rafiki Admin isn’t exposed externally and other access controls (firewalls, local-only access) ensure that the system is secured.
## [Navigation](#navigation)
[Section titled “Navigation”](#navigation)
Note
The screenshots in this guide may not perfectly reflect the appearance of the Rafiki Admin UI in all environments. Specifically, the left navigation menu can differ slightly depending on whether authentication via Kratos is enabled.
After logging in and your API credentials are configured, the left-hand navigation menu provides access to the main capabilities needed to manage your Rafiki instance.
[View full image](/img/admin-guide/home.png)
Each of the following menu items leads to a different page of the Rafiki Admin app:
* [Tenants](#tenants)
* [Assets](#assets)
* [Peers](#peers)
* [Wallet addresses](#wallet-addresses)
* [Webhook events](#webhooks)
* [Payments](#payments)
* [Account settings](#account-settings)
## [Tenants](#tenants)
[Section titled “Tenants”](#tenants)
The Tenants page allows you to manage tenants in your Rafiki instance, including viewing, creating, editing, and deleting tenants.
Note
Only users with operator privileges can create, edit, and delete tenants. An operator can’t delete themselves.
On this page, all configured tenants appear in a table where you can view the tenant’s public name, email, and status. Operators are designated by a yellow “Operator” tag.
[View full image](/img/admin-guide/tenants.png)
### [Create tenant](#create-tenant)
[Section titled “Create tenant”](#create-tenant)
To create a new tenant, select **Add tenant** from the main Tenants page.
[View full image](/img/admin-guide/create-tenant.png)
Fill out the following fields to configure and create your new tenant:
| Section | Field | Description |
| ----------------------------- | ------------------ | ---------------------------------------------------------------- |
| General Information | Public Name | A public name for the tenant. |
| | Email | The tenant’s email address. |
| Sensitive Information | API Secret | The API secret for the tenant. Treat as sensitive information. |
| Identity Provider Information | Consent URL | The URL of the tenant’s identity provider consent endpoint. |
| | Secret | The secret for the tenant’s identity provider. |
| Tenant Settings | Exchange Rates Url | The URL of the tenant’s exchange rates service. |
| | Webhook Url | The URL of the tenant’s webhook endpoint. |
| | Webhook Timeout | The timeout for the tenant’s webhook requests. |
| | Webhook Max Retry | The maximum number of retries for the tenant’s webhook requests. |
| | Wallet Address Url | The URL of the tenant’s wallet address service. |
| | ILP Address | The tenant’s Interledger Protocol (ILP) address. |
After completing these fields, select **Create** to add the new tenant.
### [Edit tenant](#edit-tenant)
[Section titled “Edit tenant”](#edit-tenant)
To edit an existing tenant, select any tenant entry from the table on the main Tenants page. This opens the Edit Tenant page where you can view and change tenant settings.
[View full image](/img/admin-guide/edit-tenant.png)
The Edit Tenant page includes the following sections:
| Section | Field | Description |
| ----------------------------- | ----------- | ------------------------------------------------------------------------------------------ |
| General Information | Tenant ID | A unique identifier assigned by Rafiki when the tenant was created. This can’t be changed. |
| | Public Name | A public name for the tenant. |
| | Email | The tenant’s email address. |
| Identity Provider Information | Consent URL | The URL of the tenant’s identity provider consent endpoint. |
| | Secret | The secret for the tenant’s identity provider. |
| Sensitive Information | API Secret | The tenant’s API credential used to authenticate requests. Read-only for operators. |
You can modify the public name, email, consent URL, and secret for a tenant. The tenant ID can’t be changed. The API secret is read-only for operators but can be rotated by the tenants when logged in under their own account.
After editing any of the preceding fields, select **Save** to commit those changes.
#### [Delete tenant](#delete-tenant)
[Section titled “Delete tenant”](#delete-tenant)
As an operator, you may need to delete tenants from your Rafiki instance. The option to delete a tenant is only visible when viewing a non-operator tenant. An operator can’t delete themselves.
To perform this irreversible action, select **Delete tenant**.

Confirm the deletion by typing “delete tenant” into the text field and selecting **Delete this tenant**.
## [Assets](#assets)
[Section titled “Assets”](#assets)
The Assets page allows you to manage assets in your Rafiki instance, including viewing, editing, and creating assets.
On this page, all configured assets appear in a table where you can view the asset ID, the asset code, the scale, and the withdrawal threshold. The list of assets shows only those for the current tenant, as determined by your API credentials.
Note
Operators can create, view, edit, and delete both their own assets and those that belong to any tenant.
Tenants can create, view, edit, and delete their own assets only.
[View full image](/img/admin-guide/assets.png)
### [Create asset](#create-asset)
[Section titled “Create asset”](#create-asset)
To create a new asset, select **Add Asset** from the main Assets page.
[View full image](/img/admin-guide/create-asset.png)
Fill out the following fields to create your new asset:
| Section | Field | Description |
| ------------------- | -------------------- | --------------------------------------------------------------------------------- |
| General Information | Code | The asset code, generally an ISO 4217 currency code where available. |
| | Scale | Difference in order of magnitude between the standard unit and a fractional unit. |
| | Withdrawal Threshold | The minimum amount of liquidity that can be withdrawn from the asset. |
| | Tenant ID | Select a tenant to associate with this asset. |
After completing these fields, select **Create** to add the new asset.
### [Edit asset](#edit-asset)
[Section titled “Edit asset”](#edit-asset)
To edit an existing asset, select any asset entry from the table on the main Assets page. This opens the Edit Asset page where you can view and change asset settings.
[View full image](/img/admin-guide/edit-asset.png)
While the Edit Asset page shares fields with the Create Asset page, it also includes fields and actions specific to managing an existing asset:
| Section | Field/Action | Description |
| --------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| General Information | Asset ID | A unique identifier assigned by Rafiki when the asset was created. This can’t be changed. |
| Liquidity Information | Amount | Current amount of asset liquidity available. |
| | Deposit Liquidity | To increase the amount of liquidity available, select **Deposit liquidity**. |
| | Withdraw Liquidity | To reduce the amount of liquidity available, select **Withdraw liquidity**. |
| Sending Fee | Fixed Fee | Flat fee per asset, specified in the smallest unit as defined by the asset scale. In our screenshot above, a fixed fee value of 100 with a scale of 2 represents 1 USD. |
| | Basis Points | A variable fee per asset. One basis point fee is equal to 0.01% of the total amount, 100 basis points = 1%, 10000 basis points = 100% |
| | Fee history | To view a list of asset fees over time, select **Fee history**. |
After editing any of the preceding fields in the General Information or Sending Fee sections, select **Save** to commit those changes.
Note
The asset code and scale can’t be modified after creation. These fields remain read-only when viewing or editing an existing asset.
#### [Delete asset](#delete-asset)
[Section titled “Delete asset”](#delete-asset)
The final section of the Edit Asset page is the irreversible action of deleting an asset. Select **Delete asset** to make this change.

Confirm the deletion by typing “delete asset” into the text field and selecting **Delete this asset**.
Prerequisites for deleting an asset
You can only delete an asset if it’s not in use. Before deletion, ensure:
* No wallet addresses are associated with the asset
* No peers reference the asset
* No payments exist that reference the asset
If the asset is still referenced, the backend prevents deletion and returns an error.
## [Peers](#peers)
[Section titled “Peers”](#peers)
The Peers page allows you to manage peering relationships in your Rafiki instance, including viewing, creating, editing, and deleting peers.
All configured peers appear in a table where you can view the peer name, its ILP address, asset details including the asset type and scale, and the outgoing HTTP endpoint. The list of peers shows only those for the current tenant, as determined by your API credentials.
Note
Operators can create, edit, and delete peers for any tenant. Tenants can only view and manage their own peers.
[View full image](/img/admin-guide/peers.png)
### [Create peer](#create-peer)
[Section titled “Create peer”](#create-peer)
To create a new peer, select **Create Peer** from the main Peers page.
[View full image](/img/admin-guide/create-peer.png)
Fill out the following fields to configure and create your peer:
| Section | Field | Description |
| ------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------- |
| General Information | Name | A public name for the peer to identify it on your system. |
| | Static ILP Address | The peer’s ILP address, obtained from the peer. |
| | Max Packet Amount | The agreed-upon maximum number of packets a payment is split into. |
| HTTP Information | Incoming Auth Tokens | A comma-separated list of tokens accepted by your Rafiki instance from a peer for incoming requests. |
| | Outgoing Auth Token | A single token used by your Rafiki instance for all outgoing requests to authenticate itself with the peer. |
| | Outgoing Endpoint | The URL of the peer’s server where your Rafiki instance sends outgoing requests. |
| Asset Information | Tenant | The tenant whose asset will be used for transactions with this peer. |
If you are an operator, you must select a tenant when creating a new peer. The asset selection dropdown is dynamically populated based on the selected tenant. If you don’t have any tenants, the tenant ID will default to your own.
After completing these fields, select **Create** to add the new peer.
### [Edit peer](#edit-peer)
[Section titled “Edit peer”](#edit-peer)
To edit an existing peer, select any peer entry from the table on the main Peers page. This opens the Edit Peer page where you can view and change peer settings.
[View full image](/img/admin-guide/edit-peer.png)
While the Edit Peer page shares fields with the Create Peer page, it also includes fields and actions specific to managing an existing peer:
| Section | Field/Action | Description |
| --------------------- | ------------------ | ---------------------------------------------------------------------------------------- |
| General Information | Peer ID | A unique identifier assigned by Rafiki when the peer was created. This can’t be changed. |
| Asset Information | View Asset | For more information about an asset, select **View asset**. |
| Liquidity Information | Amount | Current amount of peer liquidity available. |
| | Deposit Liquidity | To increase the amount of liquidity available, select **Deposit liquidity**. |
| | Withdraw Liquidity | To reduce the amount of liquidity available, select **Withdraw liquidity**. |
After editing any of the preceding fields in the General Information or HTTP Information sections, select **Save** to commit those changes.
#### [Delete peer](#delete-peer)
[Section titled “Delete peer”](#delete-peer)
The final section of the Edit Peer page is the irreversible action of deleting a peer. Select **Delete peer** to make this change.

Confirm the deletion by typing “delete peer” into the text field and selecting **Delete this peer**.
## [Wallet addresses](#wallet-addresses)
[Section titled “Wallet addresses”](#wallet-addresses)
The Wallet Addresses page allows you to manage the wallet addresses associated with your Rafiki instance, including viewing, editing, and creating wallet addresses.
On this page, all configured wallet addresses appear in the table where you can view the address URL, the public name, and the wallet status. The list of wallet addresses shows only those for the current tenant, as determined by your API credentials.
Note
Operators can create wallet addresses for any tenant. Tenants can only create their own wallet address.
[View full image](/img/admin-guide/wallet-addresses.png)
### [Create wallet address](#create-wallet-address)
[Section titled “Create wallet address”](#create-wallet-address)
To create a new wallet address, select **Create wallet address** from the main Wallet Addresses page.
[View full image](/img/admin-guide/create-wallet-address.png)
Fill out the following fields to create a new wallet address:
| Section | Field | Description |
| ------------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------- |
| General Information | Wallet address name | The case-insensitive URL of the wallet. Once set, it can’t be changed. |
| | Public name | The name associated with the wallet that will be visible to anyone with the URL. |
| | Tenant | Select a tenant to associate with this wallet. |
| | Asset | Select an asset for the wallet. The list is populated after you select a tenant and shows only that tenant’s assets. |
Wallet address requirements
* At least one asset must be created before creating a new wallet address. Refer to [Create asset](#create-asset) for more information.
* Wallet address URLs are treated as case-insensitive, meaning that both lowercase and uppercase variations of the same address will be recognized as identical.
After completing this section, select **Create** to add the new wallet address.
### [Edit wallet address](#edit-wallet-address)
[Section titled “Edit wallet address”](#edit-wallet-address)
To edit an existing wallet address, select any wallet address entry from the table on the main Wallet Addresses page. This opens the Edit Wallet Address page where you can view and change wallet address details.
[View full image](/img/admin-guide/edit-wallet-address.png)
While the Edit Wallet Address page shares fields with the Create Wallet Address page, it also includes fields and actions specific to managing an existing wallet address.
| Section | Field/Action | Description |
| --------------------- | -------------------- | ------------------------------------------------------------------------------------------ |
| General Information | ID | A unique identifier assigned by Rafiki when the wallet was created. This can’t be changed. |
| | URL | The wallet address. This can’t be changed. |
| | Status | The current status of the wallet, either active or inactive. |
| Asset Information | Code | The asset code, generally an ISO 4217 currency code where available. |
| | Scale | Difference in order of magnitude between the standard unit and a fractional unit. |
| | Withdrawal threshold | The minimum amount of liquidity that can be withdrawn from the asset. |
| | View asset | For more information about an asset, select **View asset**. |
| Liquidity Information | Amount | Current amount of liquidity available for this wallet. |
| | Withdraw | To withdraw funds from this wallet, select **Withdraw**. |
| Payments | Go to payments page | Navigates to the Payments page but with the wallet address ID filter auto-populated. |
After editing any of the preceding fields in the General Information section, select **Save** to commit those changes.
What if I need to edit or delete a wallet address?
When managing wallet addresses in Rafiki, there are certain restrictions and limitations to be aware of:
**Editing wallet address URLs**
The URL of an existing wallet address can’t be edited. Changing the URL could disrupt the transaction history associated with that wallet address, leading to inaccuracies in payment records.
**Deleting wallet addresses**
Wallet addresses can’t be deleted from the system. This restriction exists to ensure that any payments tied to a wallet address remain intact and accessible for reporting purposes.
**Solution**
In both cases, the recommended approach is to create a new wallet address and deactivate the old one. To deactivate a wallet address, change the wallet status to Inactive.
## [Webhook events](#webhook-events)
[Section titled “Webhook events”](#webhook-events)
The Webhook Events page allows you to monitor and manage webhook events in your Rafiki instance. Webhook events in Rafiki are the main communication channel between you and your Rafiki instance. See [Webhook event types](/resources/webhook-event-types) for more information about webhook events.
All triggered webhook events appear in the table. For each webhook event, you can see the webhook ID, the event type, and the date and time of the event. A field at the top of the page allows you to filter the table by event type, making it easier to drill down into specific events.
[View full image](/img/admin-guide/webhooks.png)
To view the webhook event as a JSON representation, select **View data**.
## [Payments](#payments)
[Section titled “Payments”](#payments)
The Payments page allows you to view all incoming and outgoing payments in your Rafiki instance. This allows you to monitor payment activity and track the status of payments.
All payments appear in the table. For each payment, you can view the unique payment ID, type of payment, state of the payment, and the date and time the payment was created. Fields at the top of the page allow you to filter the table by payment type and wallet address ID, making it easier to investigate specific transactions.
[View full image](/img/admin-guide/payments.png)
## [Account settings](#account-settings)
[Section titled “Account settings”](#account-settings)
The Account Settings page allows you to manage your personal account information, including updating your email address and password.
Note
The Account Settings page and its navigation menu item are only available when authentication is enabled via Ory Kratos.
[View full image](/img/admin-guide/account-settings.png)
To change your email address, enter the new email address and select **Save**.
To change your password, enter your new password and select **Save**.
# Asset liquidity
Asset liquidity is the amount of value, denominated in a given asset, that Rafiki has available to handle cross-currency (foreign exchange) transactions between you and your peer. Asset liquidity is managed separately for each tenant. Whenever an outgoing payment/incoming payment is in a different asset than the peering relationship, the liquidity of asset accounts change depending on the FX direction.
You should deposit and withdraw liquidity as necessary, based on your risk tolerance. Rafiki fails any transaction that would cause an asset’s liquidity to fall below zero.
For more information about how Rafiki handles liquidity, see the [Accounting](/overview/concepts/accounting) concepts page and the [low asset liquidity](/integration/requirements/webhook-events#low-asset-liquidity) section of the webhook events page.
## [Manage asset liquidity using Rafiki Admin](#manage-asset-liquidity-using-rafiki-admin)
[Section titled “Manage asset liquidity using Rafiki Admin”](#manage-asset-liquidity-using-rafiki-admin)
You can deposit and withdraw asset liquidity through the Rafiki Admin app’s [Assets](/admin/admin-user-guide/#edit-asset) screen. Operators can manage asset liquidity for any tenant, while tenants can only manage the liquidity of their own assets.
## [Manage asset liquidity using the Backend Admin API](#manage-asset-liquidity-using-the-backend-admin-api)
[Section titled “Manage asset liquidity using the Backend Admin API”](#manage-asset-liquidity-using-the-backend-admin-api)
Idempotency key
You must provide an `idempotencyKey` when calling any mutations related to liquidity. This unique key ensures duplicate or retried requests are processed only once. For more information, see the [Idempotency](/apis/graphql/admin-api-overview#idempotency) section in the GraphQL Admin APIs page.
### [Deposit asset liquidity](#deposit-asset-liquidity)
[Section titled “Deposit asset liquidity”](#deposit-asset-liquidity)
* Operation
```graphql
mutation DepositAssetLiquidity($input: DepositAssetLiquidityInput!) {
depositAssetLiquidity(input: $input) {
assetId
amount
id
idempotencyKey
success
}
}
```
* Variables
```json
{
"input": {
"assetId": "7b8b0f65-896d-4403-b7ba-2e24bf20eb35",
"amount": "100",
"id": "b97fd85a-126e-42ef-b40d-1a50a70ffa6f",
"idempotencyKey": "b97fd85a-126e-42ef-b40d-1a50a70ffa6f",
}
}
```
For more information about this mutation’s input object, see [`DepositAssetLiquidityInput`](https://rafiki.dev/apis/graphql/backend/inputobjects/#depositassetliquidityinput).
* Response
When an asset liquidity deposit is successful, `DepositAssetLiquidity` returns `true`.
```json
{
data: {
success: true
}
}
```
### [Withdraw asset liquidity](#withdraw-asset-liquidity)
[Section titled “Withdraw asset liquidity”](#withdraw-asset-liquidity)
* Operation
```graphql
mutation CreateAssetLiquidityWithdrawal($input: CreateAssetLiquidityWithdrawalInput!) {
createAssetLiquidityWithdrawal(input: $input) {
success
}
}
```
* Variables
```json
{
"input": {
"id": "b97fd85a-126e-42ef-b40d-1a50a70ffa6f",
"assetId": "7b8b0f65-896d-4403-b7ba-2e24bf20eb35",
"amount": "100",
"idempotencyKey": "b97fd85a-126e-42ef-b40d-1a50a70ffa6f",
"timeoutSeconds": 0
}
}
```
For more information about this mutation’s input object, see [`CreateAssetLiquidityWithdrawalInput`](https://rafiki.dev/apis/graphql/backend/inputobjects/#createassetliquiditywithdrawalinput).
* Response
When an asset liquidity withdrawal is successful, `CreateAssetLiquidityWithdrawal` returns `true`.
```json
{
data: {
success: true
}
}
```
# Payment liquidity
Payment liquidity represents:
* The value received from a completed incoming payment
* The value available to send in an outgoing payment
Because Rafiki doesn’t hold funds, anything you receive in an incoming payment must be withdrawn and then credited to the recipient’s account on your ledger. Listen for the [incoming payments](/integration/requirements/webhook-events#incoming-payments) webhook events to know when you need to interact with Rafiki.
Any excess liquidity that remains after an outgoing payment completes must be withdrawn. You may also find that you must deposit liquidity into Rafiki to fund an outgoing payment. Listen for Rafiki’s [outgoing payments](/integration/requirements/webhook-events#outgoing-payments) webhook events to know when action is required on your part.
Rafiki Admin
The Rafiki Admin doesn’t allow you to manage payment liquidity, but you can view details about incoming and outgoing payments through the app’s [Payments](/admin/admin-user-guide#payments) screen.
For more information about how Rafiki handles liquidity, see the [Accounting](/overview/concepts/accounting) concepts page.
## [Manage payment liquidity using the Backend Admin API](#manage-payment-liquidity-using-the-backend-admin-api)
[Section titled “Manage payment liquidity using the Backend Admin API”](#manage-payment-liquidity-using-the-backend-admin-api)
Idempotency key
You must provide an `idempotencyKey` when calling any mutations related to liquidity. This unique key ensures duplicate or retried requests are processed only once. For more information, see the [Idempotency](/apis/graphql/admin-api-overview#idempotency) section in the GraphQL Admin APIs page.
### [Withdraw incoming payment liquidity](#withdraw-incoming-payment-liquidity)
[Section titled “Withdraw incoming payment liquidity”](#withdraw-incoming-payment-liquidity)
* Operation
```graphql
mutation CreateIncomingPaymentWithdrawal($input: CreateIncomingPaymentWithdrawalInput!) {
createIncomingPaymentWithdrawal(input: $input) {
success
}
}
```
* Variables
```json
{
"input": {
"incomingPaymentId": "b4f85d5c-652d-472d-873c-4ba2a5e39052",
"idempotencyKey": "a09b730d-8610-4fda-98fa-ec7acb19c775",
"timeoutSeconds": 0
}
}
```
For more information about this mutation’s input object, see [`CreateIncomingPaymentWithdrawalInput`](https://rafiki.dev/apis/graphql/backend/inputobjects/#createincomingpaymentwithdrawalinput).
* Response
When an incoming payment liquidity withdrawal is successful, `CreateIncomingPaymentWithdrawal` returns `true`.
```json
{
data: {
success: true
}
}
```
### [Deposit outgoing payment liquidity](#deposit-outgoing-payment-liquidity)
[Section titled “Deposit outgoing payment liquidity”](#deposit-outgoing-payment-liquidity)
* Operation
```graphql
mutation DepositOutgoingPaymentLiquidity($input: DepositOutgoingPaymentLiquidityInput!) {
depositOutgoingPaymentLiquidity(input: $input) {
success
}
}
```
* Variables
```json
{
"input": {
"outgoingPaymentId": "b4f85d5c-652d-472d-873c-4ba2a5e39052",
"idempotencyKey": "a09b730d-8610-4fda-98fa-ec7acb19c775",
"dataToTransmit": "xyz789"
}
}
```
For more information about this mutation’s input object, see [`DepositOutgoingPaymentLiquidityInput`](https://rafiki.dev/apis/graphql/backend/inputobjects/#depositoutgoingpaymentliquidityinput).
* Response
When an outgoing payment liquidity deposit is successful, `DepositOutgoingPaymentLiquidity` returns `true`.
```json
{
data: {
success: true
}
}
```
### [Withdraw outgoing payment liquidity](#withdraw-outgoing-payment-liquidity)
[Section titled “Withdraw outgoing payment liquidity”](#withdraw-outgoing-payment-liquidity)
* Operation
```graphql
mutation CreateOutgoingPaymentWithdrawal($input: CreateOutgoingPaymentWithdrawalInput!) {
createOutgoingPaymentWithdrawal(input: $input) {
success
}
}
```
* Variables
```json
{
"input": {
"outgoingPaymentId": "b4f85d5c-652d-472d-873c-4ba2a5e39052",
"idempotencyKey": "a09b730d-8610-4fda-98fa-ec7acb19c775",
"timeoutSeconds": 0
}
}
```
For more information about this mutation’s input object, see [`CreateOutgoingPaymentWithdrawalInput`](https://rafiki.dev/apis/graphql/backend/inputobjects/#createoutgoingpaymentwithdrawalinput).
* Response
When an outgoing payment liquidity withdrawal is successful, `CreateOutgoingPaymentWithdrawal` returns `true`.
```json
{
data: {
success: true
}
}
```
# Peer liquidity
Peer liquidity is the line of credit you extend to a peer, denominated in your agreed upon asset. Peer liquidity is managed separately for each tenant. A peer’s liquidity account balance represents the amount of credit the peer still has available to them.
A peer’s liquidity increases when payments are made to the peer and decreases when payments are made from the peer. For example, if a customer of your peer sends your customer a payment of $20 USD, then your peer’s liquidity account decreases by 20.
If a peer’s liquidity is insufficient (for example, they’ve used up their allotted credit line), transactions initiated from the peer will fail. Once a peer’s liquidity is used up, you should settle with your peer and reset their liquidity. Deposit and withdraw peer liquidity as necessary, based on your risk profile.
For more information about how Rafiki handles liquidity, see the [Accounting](/overview/concepts/accounting) concepts page and the [low peer liquidity](/integration/requirements/webhook-events#low-peer-liquidity) section of the webhook events page.
## [Manage peer liquidity using Rafiki Admin](#manage-peer-liquidity-using-rafiki-admin)
[Section titled “Manage peer liquidity using Rafiki Admin”](#manage-peer-liquidity-using-rafiki-admin)
You can deposit and withdraw peer liquidity through the Rafiki Admin app’s [Peers](/admin/admin-user-guide/#edit-peer) screen. Operators can manage peer liquidity for any tenant, while tenants can’t manage peer liquidity at all.
## [Manage peer liquidity using the Backend Admin API](#manage-peer-liquidity-using-the-backend-admin-api)
[Section titled “Manage peer liquidity using the Backend Admin API”](#manage-peer-liquidity-using-the-backend-admin-api)
Idempotency key
You must provide an `idempotencyKey` when calling any mutations related to liquidity. This unique key ensures duplicate or retried requests are processed only once. For more information, see the [Idempotency](/apis/graphql/admin-api-overview#idempotency) section in the GraphQL Admin APIs page.
### [Deposit peer liquidity](#deposit-peer-liquidity)
[Section titled “Deposit peer liquidity”](#deposit-peer-liquidity)
* Operation
```graphql
mutation DepositPeerLiquidity($input: DepositPeerLiquidityInput!) {
depositPeerLiquidity(input: $input) {
success
}
}
```
* Variables
```json
{
"input": {
"id": "a09b730d-8610-4fda-98fa-ec7acb19c775",
"peerId": "73158598-2e0c-4973-895e-aebd115af260",
"amount": "1000000",
"idempotencyKey": "a09b730d-8610-4fda-98fa-ec7acb19c775"
}
}
```
For more information about this mutation’s input object, see [`DepositPeerLiquidityInput`](https://rafiki.dev/apis/graphql/backend/inputobjects/#depositpeerliquidityinput).
* Response
When a peer liquidity deposit is successful, `DepositPeerLiquidity` returns `true`.
```json
{
"data": {
"success": true
}
}
```
### [Withdraw peer liquidity](#withdraw-peer-liquidity)
[Section titled “Withdraw peer liquidity”](#withdraw-peer-liquidity)
* Operation
```graphql
mutation CreatePeerLiquidityWithdrawal($input: CreatePeerLiquidityWithdrawalInput!) {
createPeerLiquidityWithdrawal(input: $input) {
success
}
}
```
* Variables
```json
{
"input": {
"id": "421fae87-9a59-4217-9ff8-faf55ffab9c6",
"peerId": "73158598-2e0c-4973-895e-aebd115af260",
"amount": "100",
"idempotencyKey": "b97fd85a-126e-42ef-b40d-1a50a70ffa6f",
"timeoutSeconds": 0
}
}
```
For more information about this mutation’s input object, see [`CreatePeerLiquidityWithdrawalInput`](https://rafiki.dev/apis/graphql/backend/inputobjects/#createpeerliquiditywithdrawalinput).
* Response
When a peer liquidity withdrawal is successful, `CreatePeerLiquidityWithdrawal` returns `true`.
```json
{
"data": {
"success": true
}
}
```
# Two-phase transfers
Rafiki allows for two-phase transfers, which moves funds in two stages.
1. Reserve funds (`pending`)
2. Resolve funds (`post`, `void`, or `expire`)
The following transactions support two-phase transfers:
* Asset liquidity withdrawal
* Peer liquidity withdrawal
* Incoming payment withdrawal
* Outgoing payment withdrawal
* Wallet address withdrawal
When a withdraw-liquidity transaction is requested with a timeout greater than `0`, the transaction processes as a two-phase transfer. A `0` denotes the absence of a timeout.
If the timeout interval passes before the transaction posts or is voided, the transaction expires and the full amount is returned to the original account.
## [Manage two-phase transfers using the Backend Admin API](#manage-two-phase-transfers-using-the-backend-admin-api)
[Section titled “Manage two-phase transfers using the Backend Admin API”](#manage-two-phase-transfers-using-the-backend-admin-api)
Idempotency key
You must provide an `idempotencyKey` when calling any mutations related to liquidity. This unique key ensures duplicate or retried requests are processed only once. For more information, see the [Idempotency](/apis/graphql/admin-api-overview#idempotency) section in the GraphQL Admin APIs page.
### [Post and commit a successful transfer](#post-and-commit-a-successful-transfer)
[Section titled “Post and commit a successful transfer”](#post-and-commit-a-successful-transfer)
* Operation
```graphql
mutation PostLiquidityWithdrawal($input: PostLiquidityWithdrawalInput!) {
postLiquidityWithdrawal(input: $input) {
error
success
}
}
```
* Variables
```json
{
"input": {
"withdrawalId": "b4f85d5c-652d-472d-873c-4ba2a5e39052",
"idempotencyKey": "a09b730d-8610-4fda-98fa-ec7acb19c775"
}
}
```
For more information about this mutation’s input object, see [`PostLiquidityWithdrawalInput`](https://rafiki.dev/apis/graphql/backend/inputobjects/#postliquiditywithdrawalinput).
* Response
When a liquidity withdrawal post is successful, `PostLiquidityWithdrawal` returns `true`.
```json
{
data: {
success: true
}
}
```
### [Void and roll-back an unsuccessful transfer](#void-and-roll-back-an-unsuccessful-transfer)
[Section titled “Void and roll-back an unsuccessful transfer”](#void-and-roll-back-an-unsuccessful-transfer)
* Operation
```graphql
mutation VoidLiquidityWithdrawal($input: VoidLiquidityWithdrawalInput!) {
voidLiquidityWithdrawal(input: $input) {
error
success
}
}
```
* Variables
```json
{
"input": {
"withdrawalId": "b4f85d5c-652d-472d-873c-4ba2a5e39052",
"idempotencyKey": "a09b730d-8610-4fda-98fa-ec7acb19c775"
}
}
```
For more information about this mutation’s input object, see [`VoidLiquidityWithdrawalInput`](https://rafiki.dev/apis/graphql/backend/inputobjects/#voidliquiditywithdrawalinput).
* Response
When a liquidity withdrawal is successfully voided and rolled back, `VoidLiquidityWithdrawal` returns `true`.
```json
{
data: {
success: true
}
}
```
# Overview
Rafiki provides two GraphQL APIs, described below. As described on [GraphQL.org](https://graphql.org/), GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL APIs are organized in terms of types and fields, not endpoints.
## [Backend Admin API](#backend-admin-api)
[Section titled “Backend Admin API”](#backend-admin-api)
The Backend Admin API provides you with comprehensive capabilities to manage your Rafiki instance. Core capabilities include managing tenants, peering relationships, assets, wallet addresses and their public keys, as well as liquidity management through deposits and withdrawals, all in a tenant’s context.
All requests to the Backend Admin API are HMAC-signed using the tenant’s API secret and include a `tenant-id` header. See [Backend service](/integration/deployment/services/backend-service#authentication-and-authorization) for more information.
This API is complemented by the Rafiki Admin app, a frontend interface for Rafiki management that directly interacts with the Backend Admin API. Check out the [Rafiki Admin Application User Guide](/admin/admin-user-guide) for more information.
## [Auth Admin API](#auth-admin-api)
[Section titled “Auth Admin API”](#auth-admin-api)
The Auth Admin API allows you to get information about a grant, such as its status, state, related payment details, and the wallet address of the grantee’s account. The API also allows you to revoke grants.
## [Idempotency](#idempotency)
[Section titled “Idempotency”](#idempotency)
Wikipedia describes [idempotence](https://en.wikipedia.org/wiki/Idempotence) as being the property of “certain operations in…computer science whereby \[the operations] can be applied multiple times without changing the result beyond the initial application.” “An operation can be repeated or retried as often as necessary without causing unintended effects. With non-idempotent operations, the algorithm may have to keep track of whether the operation was already performed.”
Several mutations in the Admin APIs use an idempotency key to allow for safely retrying requests without performing operations multiple times. The key must be unique (for example, a UUID v4).
For the Admin APIs, whenever a mutation with an `idempotencyKey` is called, the request payload and the request response are saved under that key. Any subsequent requests made with the same idempotency key will return the original response and status of the request, regardless of whether the request was successful. Keys are cached for a default of 24 hours. The default can be changed via the `backend` service’s `GRAPHQL_IDEMPOTENCY_KEY_TTL_MS` environment flag.
If a request is made while the first request under the same `idempotencyKey`, is still processing, the APIs return an error to prevent concurrent duplicates. The concurrency lock duration is `2` seconds by default and can be changed via the `backend` service’s `GRAPHQL_IDEMPOTENCY_KEY_LOCK_MS` environment flag.
# Signing Admin API requests
Rafiki requires every request to the Backend Admin API to include a valid cryptographic signature.
This signature authenticates the caller and ensures the request body wasn’t altered in transit.
## [Signature overview](#signature-overview)
[Section titled “Signature overview”](#signature-overview)
Each request is HMAC-signed with SHA-256 using the tenant’s or operator’s API secret. This signature ensures that Rafiki can verify where the request originated and confirm that the payload data matches what was originally signed.
All signed requests include two headers: the `signature` header and the `tenant-id` header.
### [`signature` header](#signature-header)
[Section titled “signature header”](#signature-header)
The `signature` header authenticates the request payload. Rafiki uses this value to verify the request’s integrity.
signature header
```bash
signature: t=, v=
```
* `t=`: The UNIX timestamp (in seconds) when the signature was generated.
* `v=`: The versioned HMAC SHA-256 signature digest. The default version is v1.
### [`tenant-id` header](#tenant-id-header)
[Section titled “tenant-id header”](#tenant-id-header)
tenant-id header
```bash
tenant-id:
```
* ``: The unique UUID v4 identifying the tenant or operator.
Rafiki uses the signature to authenticate the tenant or operator and prevent replay attacks.
## [How signing works](#how-signing-works)
[Section titled “How signing works”](#how-signing-works)
To protect the Admin API from unauthorized or replayed requests, each client request must include a digital signature that Rafiki can verify. By generating a signature before sending your request, you allow Rafiki to confirm who sent it and ensure the payload matches what was originally signed.
Follow these steps to create a valid signature.
### [Generate the timestamp](#generate-the-timestamp)
[Section titled “Generate the timestamp”](#generate-the-timestamp)
Each signature includes a timestamp representing when it was created. Rafiki uses this value to confirm the request is recent and reject requests outside the configured TTL window (30 seconds by default).
In JavaScript, generate it with `Date.now()`.
### [Prepare the request body](#prepare-the-request-body)
[Section titled “Prepare the request body”](#prepare-the-request-body)
To ensure you and Rafiki sign the same data, serialize the GraphQL request consistently.
Use a canonicalization method that orders keys predictably (for example, [`json‑canonicalize`](https://www.npmjs.com/package/json-canonicalize)) applied to the `query`, `variables`, and `operationName` fields.
### [Build the payload string](#build-the-payload-string)
[Section titled “Build the payload string”](#build-the-payload-string)
Combine the timestamp and canonicalized request body with a period (.). This string forms the message to be signed.
```bash
.
```
### [Create the HMAC digest](#create-the-hmac-digest)
[Section titled “Create the HMAC digest”](#create-the-hmac-digest)
Generate the digest using the tenant’s or operator’s API secret as the key and the payload string as the message. The output should be a hexadecimal string.
### [Attach signature headers](#attach-signature-headers)
[Section titled “Attach signature headers”](#attach-signature-headers)
Include the generated values in your request headers:
```bash
signature: t=, v=
tenant-id:
```
The version number (`v1` by default) corresponds to the configured `ADMIN_API_SIGNATURE_VERSION` environment variable.
Rafiki reconstructs the same payload internally and validates the digest, timestamp, and tenant ID before processing the request.
## [Example implementation](#example-implementation)
[Section titled “Example implementation”](#example-implementation)
Below is an example in JavaScript to sign an Admin API request:
Signing Admin API request example
```js
import { createHmac } from 'crypto'
import { canonicalize } from 'json-canonicalize'
import { gql } from '@apollo/client'
import { print } from 'graphql/language/printer'
const timestamp = Date.now()
const version = process.env.ADMIN_API_SIGNATURE_VERSION
const GET_ASSET = gql`
query GetAsset($id: String!) {
asset(id: $id) {
id
code
scale
}
}
`
const requestBody = {
query: print(GET_ASSET), // converts `DocumentNode` to string
variables: { id: 'asset-id-here' },
operationName: 'GetAsset'
}
// Canonicalize ensures both client and server produce identical JSON strings
// by sorting object keys deterministically and normalizing whitespace.
const payload = `${timestamp}.${canonicalize(requestBody)}`
const hmac = createHmac('sha256', process.env.ADMIN_API_SECRET)
hmac.update(payload)
const digest = hmac.digest('hex')
headers['signature'] = `t=${timestamp}, v${version}=${digest}`
headers['tenant-id'] = process.env.OPERATOR_TENANT_ID
```
### [Configuration reference](#configuration-reference)
[Section titled “Configuration reference”](#configuration-reference)
| Environment variable | Description | Default |
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `ADMIN_API_SIGNATURE_VERSION` | The version of the HMAC SHA-256 request-signing algorithm used by the Backend Admin API. | `1` |
| `ADMIN_API_SECRET` | Operator API secret used to sign Backend Admin API requests (HMAC SHA‑256). Set to a strong, random value. Synced to the operator tenant on startup. | — |
| `OPERATOR_TENANT_ID` | The unique identifier of the operator. Must be a UUID v4 generated by the operator. | — |
## [Signature validation](#signature-validation)
[Section titled “Signature validation”](#signature-validation)
When Rafiki receives a signed Admin API request, it automatically rebuilds the same payload and verifies the HMAC digest against the operator’s configured secret.
The request is accepted only if the following conditions are met:
* The signature digest matches
* The timestamp is in the allowed TTL window
* The tenant ID is recognized
If any check fails, Rafiki rejects the request before executing any GraphQL operation.
For details on how Rafiki validates incoming requests from its own services, see [Verify webhook signatures](/integration/requirements/webhook-events/#verify-webhook-signatures)
# Docker Compose
This guide is an example of deploying Rafiki using Docker Compose with Nginx as a reverse proxy on a virtual machine (VM) in a cloud environment. This guide also uses [Certbot](https://certbot.eff.org/) to generate [Let’s Encrypt TLS certificates](https://letsencrypt.org/) to secure exposed ports using HTTPS.
From [Docker’s documentation](https://docs.docker.com/compose/), Docker Compose is a tool for defining and running multi-container applications using a single YAML file. It simplifies the process of configuring and running multiple services.
Note
While Docker Compose could serve as a production deployment, it’s an environment best suited for development and testing. For more information, refer to Docker’s [documentation](https://docs.docker.com/compose/intro/features-uses/#common-use-cases-of-docker-compose) on common use cases.
## [Prerequisites](#prerequisites)
[Section titled “Prerequisites”](#prerequisites)
### [Deploy VM and install Docker](#deploy-vm-and-install-docker)
[Section titled “Deploy VM and install Docker”](#deploy-vm-and-install-docker)
Recommended software version
We recommended using the latest vendor supported version for each of the software dependencies listed in this section.
Deploy a general purpose VM with the following minimum specifications:
* OS: Linux distro
* RAM: 4 GB
* vCPUs: 2
Install the following software on the VM:
* [Docker Engine](https://docs.docker.com/engine/install/)
* [Docker Compose](https://docs.docker.com/compose/install/#scenario-two-install-the-compose-plugin)
### [Install Nginx and Certbot](#install-nginx-and-certbot)
[Section titled “Install Nginx and Certbot”](#install-nginx-and-certbot)
Once you have provisioned the VM in your cloud environment, install Nginx along with Certbot:
```sh
sudo apt update && sudo apt install nginx certbot python3-certbot-nginx
```
### [Domain preparation](#domain-preparation)
[Section titled “Domain preparation”](#domain-preparation)
Generate the Let’s Encrypt certificates using Certbot:
```sh
certbot certonly --manual --preferred-challenges=dns --email EMAIL --server https://acme-v02.api.letsencrypt.org/directory --agree-tos -d DOMAIN
```
Update TXT record
Domain can be in wildcard format. You will also need to update the TXT record in this step.
As Let’s Encrypt certificates are valid for 90 days, you must set up a cron process to renew the certificate on a regular schedule:
```sh
crontab -e
0 3 * * * certbot renew
```
### [Domain and DNS configuration](#domain-and-dns-configuration)
[Section titled “Domain and DNS configuration”](#domain-and-dns-configuration)
Map the [Open Payments resource server](/integration/deployment/services/backend-service#open-payments) to your domain, and the [ILP connector](/integration/deployment/services/backend-service#interledger-connector), [Open Payments auth server](/integration/deployment/services/auth-service), and [Admin UI](/integration/deployment/services/frontend-service) to subdomains. Using the DNS host of your choice, set up your domain and subdomains according to the following recommended convention:
| Service | Exposes | URL | Example |
| ----------------------------- | ----------------------------------------------------------------- | ------------ | ------------------ |
| Open Payments resource server | Open Payments APIs | DOMAIN | myrafiki.com |
| ILP connector | ILP connector to send and receive ILP packets between peers | ilp.DOMAIN | ilp.myrafiki.com |
| Open Payments auth server | Reference implementation of an Open Payments authorization server | auth.DOMAIN | auth.myrafiki.com |
| Admin UI | Admin UI to manage Rafiki | admin.DOMAIN | admin.myrafiki.com |
Note
The example domain and subdomain values are for demonstration purposes only. You must use the actual domain names that you set up with your DNS host.
Next, update the DNS records (A records) to point to the static external IP address of the virtual machine according to the table above.
## [Configure Compose file](#configure-compose-file)
[Section titled “Configure Compose file”](#configure-compose-file)
The Docker Compose file is a YAML configuration file used to define the services, networks, and volumes that make up a multi-container app. In this section, we’ll explore the Compose file by breaking it down into the individual Rafiki services.
Update Compose file
Before using the Compose file, you must update the variables with values relevant to your environment. Specifically, change the values enclosed in the brackets and substitute `newest-version` with the latest Rafiki [version](https://github.com/interledger/rafiki/releases).
### [Docker Compose example](#docker-compose-example)
[Section titled “Docker Compose example”](#docker-compose-example)
While the actual Compose file is a single YAML file containing all the services, this page will guide you through each service one by one. For each service, we’ll look at the relevant configuration details along with the corresponding environment variables.
#### [Auth service](#auth-service)
[Section titled “Auth service”](#auth-service)
The Rafiki `auth` service is responsible for handling authentication and authorization for your app. It connects to a Postgres database to store auth-related resources and a Redis database for storing session data. See [Auth service](/integration/deployment/services/auth-service/) for more information.
Ports exposed:
* 3003 (`ADMIN_PORT`) is used for the Auth Admin API
* 3006 (`AUTH_PORT`) is used for the Open Payments authorization server
Make sure to configure the `AUTH_DATABASE_URL` and `REDIS_URL` environment variables to point to your database instances.
Running Rafiki behind a proxy
If you plan to run your Rafiki instance behind a proxy, you must set the `TRUST_PROXY` variable to `true`
```sh
rafiki-auth:
image: ghcr.io/interledger/rafiki-auth:
container_name: rafiki-auth
environment:
AUTH_DATABASE_URL: {postgresql://postgres:password@localhost:5432/auth_development}
AUTH_SERVER_URL: {https://auth.myrafiki.com}
ADMIN_PORT: 3003
AUTH_PORT: 3006
INTROSPECTION_PORT: 3007
INTERACTION_PORT: 3009
COOKIE_KEY: {...}
IDENTITY_SERVER_SECRET: {...}
IDENTITY_SERVER_URL: {https://idp.mysystem.com}
REDIS_URL: {redis://127.0.0.1:6379}
TRUST_PROXY: true
depends_on:
- postgres
networks:
- rafiki
ports:
- '3003:3003'
- '3006:3006'
- '3007:3007'
- '3009:3009'
restart: always
```
##### [Environment variables](#environment-variables)
[Section titled “Environment variables”](#environment-variables)
| Variable | Required | Description |
| --------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ADMIN_API_SECRET` | Y | Operator API secret used to sign Auth Admin API requests (HMAC SHA‑256). Set to a strong, random value. Synced to the operator tenant on startup. |
| `AUTH_DATABASE_URL` | Y | The URL of the Postgres database storing your Open Payments grant data. |
| `AUTH_SERVER_URL` | Y | The public endpoint for your Rafiki instance’s public Open Payments routes. |
| `COOKIE_KEY` | Y | The [koa KeyGrip key](https://koajs.com/#app-keys-) that’s used to sign cookies for an interaction session. |
| `IDENTITY_SERVER_SECRET` | Y | A shared secret between the authorization server and the IdP server; the authorization server will use the secret to secure its IdP-related endpoints. When the IdP server sends requests to the authorization server, the IdP server must provide the secret via an [`x-idp-secret`](/integration/requirements/open-payments/idp#x-idp-secret-header) header. |
| `IDENTITY_SERVER_URL` | Y | The URL of your IdP’s server, used by the authorization server to tell an Open Payments client where to redirect the end-user to start interactions. |
| `OPERATOR_TENANT_ID` | Y | The unique identifier of the operator. Must be a UUID v4 generated by the operator. |
| `REDIS_URL` | Y | The connection URL of Redis. |
| `ACCESS_TOKEN_DELETION_DAYS` | N | The days until expired and/or revoked access tokens are deleted. |
| `ACCESS_TOKEN_EXPIRY_SECONDS` | N | The expiry time, in seconds, for access tokens. |
| `ADMIN_API_SIGNATURE_TTL_SECONDS` | N | The TTL, in seconds, for which an Auth Admin API request signature is valid. |
| `ADMIN_API_SIGNATURE_VERSION` | N | The version of the HMAC SHA-256 request-signing algorithm used by the Auth Admin API. |
| `ADMIN_PORT` | N | The port of your Rafiki Auth Admin API server. |
| `AUTH_PORT` | N | The port of your Open Payments authorization server. |
| `INCOMING_PAYMENT_INTERACTION` | N | When `true`, incoming Open Payments grant requests are interactive. |
| `INCOMING_PAYMENT_WORKERS` | N | The number of workers processing incoming payment requests. |
| `INTERACTION_EXPIRY_SECONDS` | N | The time, in seconds, for which a user can interact with a grant request before the request expires. |
| `INTERACTION_PORT` | N | The port number of your Open Payments interaction-related APIs. |
| `INTROSPECTION_PORT` | N | The port of your Open Payments access token introspection server. |
| `LIST_ALL_ACCESS_INTERACTION` | N | When `true`, grant requests that include a `list-all` action will require interaction. In these requests, the client asks to list resources that it didn’t create. |
| `LOG_LEVEL` | N | [Pino log level](https://getpino.io/#/docs/api?id=levels). |
| `NODE_ENV` | N | The type of node environment: `development`, `test`, or `production`. |
| `QUOTE_INTERACTION` | N | When `true`, quote grants are interactive. |
| `REDIS_TLS_CA_FILE_PATH` | N | [Redis TLS config](https://redis.io/docs/latest/operate/oss_and_stack/management/security/encryption/). |
| `REDIS_TLS_CERT_FILE_PATH` | N | [Redis TLS config](https://redis.io/docs/latest/operate/oss_and_stack/management/security/encryption/). |
| `REDIS_TLS_KEY_FILE_PATH` | N | [Redis TLS config](https://redis.io/docs/latest/operate/oss_and_stack/management/security/encryption/). |
| `TRUST_PROXY` | N | Must be set to `true` when running Rafiki behind a proxy. When `true`, the `X-Forwarded-Proto` header is used to determine if connections are secure. |
| `WAIT_SECONDS` | N | The wait time, in seconds, included in a grant request response (`grant.continue`). |
#### [Backend service](#backend-service)
[Section titled “Backend service”](#backend-service)
The Rafiki `backend` service handles business logic and external communication. It exposes the Open Payments APIs and an Interledger connector for sending and receiving packets. It connects to a Redis database for caching, a Postgres database for Open Payments resources, and TigerBeetle for accounting liquidity. See [Backend service](/integration/deployment/services/backend-service) for more information.
TigerBeetle or Postgres for accounting database
TigerBeetle is recommended, but if you would rather use Postgres as an accounting database make sure to set `USE_TIGERBEETLE` to false.
Ports exposed:
* 3000 (`OPEN_PAYMENTS_PORT`) is used for the Open Payments resource server
* 3001 (`ADMIN_PORT`) is used for the Backend Admin API
* 3002 (`CONNECTOR_PORT`) is used for the ILP connector to send and receive ILP packets
Make sure to configure the `DATABASE_URL` and `REDIS_URL` environment variables to point to your database instances.
```sh
rafiki-backend:
image: ghcr.io/interledger/rafiki-backend:
container_name: rafiki-backend
depends_on:
- postgres
- redis
environment:
AUTH_SERVER_GRANT_URL: {https://auth.myrafiki.com}
AUTH_SERVER_INTROSPECTION_URL: {https://auth.myrafiki.com/3007}
DATABASE_URL: {postgresql://postgres:password@localhost:5432/development}
ILP_ADDRESS: {test.myrafiki}
ADMIN_PORT: 3001
CONNECTOR_PORT: 3002
OPEN_PAYMENTS_PORT: 3000
OPEN_PAYMENTS_URL: {https://myrafiki.com}
REDIS_URL: {redis://127.0.0.1:6379}
WALLET_ADDRESS_URL: {https://myrafiki.com/rafiki-instance}
WEBHOOK_URL: {https://mysystem.com/webhooks}
EXCHANGE_RATES_URL: {https://mysystem.com/rates}
ILP_CONNECTOR_URL: {https://ilp.myrafiki.com}
INSTANCE_NAME: {'My ASE name'}
TRUST_PROXY: true
KEY_ID: ...
USE_TIGERBEETLE: true
TIGERBEETLE_CLUSTER_ID: 0
TIGERBEETLE_REPLICA_ADDRESSES: 10.5.0.50:4342
networks:
- rafiki
ports:
- '3000:3000'
- '3001:3001'
- '3002:3002'
privileged: true
restart: always
volumes:
- ../temp/:/workspace/temp/
```
##### [Environment variables](#environment-variables-1)
[Section titled “Environment variables”](#environment-variables-1)
| Variable | Required | Description |
| ----------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ADMIN_API_SECRET` | Y | Operator API secret used to sign Backend Admin API requests (HMAC SHA‑256). Set to a strong, random value. Synced to the operator tenant on startup. |
| `AUTH_SERVICE_API_URL` | Y | The service-to-service API endpoint for propagating tenant information changes to the `auth` server. |
| `AUTH_SERVER_GRANT_URL` | Y | The endpoint on your Open Payments authorization server to grant a request. |
| `AUTH_SERVER_INTROSPECTION_URL` | Y | The endpoint on your Open Payments authorization server to introspect an access token. |
| `DATABASE_URL` | Y | The Postgres database URL of the database storing your resource data. |
| `EXCHANGE_RATES_URL` | Y | The endpoint your Rafiki instance uses to request exchange rates. |
| `ILP_ADDRESS` | Y | The ILP address of your Rafiki instance. |
| `ILP_CONNECTOR_URL` | Y | The ILP connector address where ILP packets are received. |
| `KEY_ID` | Y | Your Rafiki instance’s client key ID. |
| `OPERATOR_TENANT_ID` | Y | The unique identifier of the operator. Must be a UUID v4 generated by the operator. |
| `OPEN_PAYMENTS_URL` | Y | The public endpoint of your Open Payments resource server. |
| `REDIS_URL` | Y | The Redis URL of the database handling ILP packet data. |
| `USE_TIGERBEETLE` | Y | When `true`, a TigerBeetle database is used for accounting. When `false`, a Postgres database is used. |
| `WEBHOOK_URL` | Y | Your endpoint that consumes webhook events. |
| `ADMIN_PORT` | N | The port of your Backend Auth API server. |
| `ADMIN_API_SIGNATURE_VERSION` | N | The version of the request signing algorithm used to generate signatures. |
| `AUTO_PEERING_SERVER_PORT` | N | If autopeering is enabled, the server will use this port. |
| `CONNECTOR_PORT` | N | The port of the ILP connector for sending packets via ILP over HTTP. |
| `ENABLE_AUTO_PEERING` | N | When `true`, autopeering is enabled. |
| `ENABLE_MANUAL_MIGRATIONS` | N | When `true`, you must run the database manually with the command `npm run knex – migrate:latest –env production` |
| `ENABLE_SPSP_PAYMENT_POINTERS` | N | When `true`, the SPSP route is enabled. |
| `ENABLE_TELEMETRY` | N | Enables the telemetry service on Rafiki. |
| `ENABLE_TELEMETRY_TRACES` | N | N/A |
| `EXCHANGE_RATES_LIFETIME` | N | The time, in milliseconds, the exchange rates you provide via the `EXCHANGE_RATES_URL` are valid. |
| `GRAPHQL_IDEMPOTENCY_KEY_LOCK_MS` | N | The TTL, in milliseconds, for `idempotencyKey` concurrency lock on GraphQL mutations on the Backend Admin API. |
| `GRAPHQL_IDEMPOTENCY_KEY_TTL_MS` | N | The TTL, in milliseconds, for `idempotencyKey` on GraphQL mutations on the Backend Admin API. |
| `INCOMING_PAYMENT_CREATED_POLL_FREQUENCY_MS` | N | N/A |
| `INCOMING_PAYMENT_CREATED_POLL_TIMEOUT_MS` | N | N/A |
| `INCOMING_PAYMENT_EXPIRY_MAX_MS` | N | The maximum into the future, in milliseconds, incoming payments expiry can be set to on creation. |
| `INCOMING_PAYMENT_WORKER_IDLE` | N | The time, in milliseconds, that `INCOMING_PAYMENT_WORKERS` will wait until checking an empty incoming payment request queue again. |
| `INCOMING_PAYMENT_WORKERS` | N | The number of workers processing incoming payment requests. |
| `INSTANCE_NAME` | N | Your Rafiki instance’s name used to communicate for autopeering and/or [telemetry](/overview/concepts/telemetry). Required when autopeering and/or telemetry is enabled. |
| `LOG_LEVEL` | N | [Pino log level](https://getpino.io/#/docs/api?id=levels) |
| `MAX_OUTGOING_PAYMENT_RETRY_ATTEMPTS` | N | Specifies how many times an outgoing payment is retried before failing completely. |
| `NODE_ENVIRONMENT` | N | The type of node environment: `development`, `test`, or `production`. |
| `OPEN_PAYMENTS_PORT` | N | The port of your Open Payments resource server. |
| `OPEN_TELEMETRY_COLLECTOR_URLS` | N | N/A |
| `OPEN_TELEMETRY_EXPORT_INTERVAL` | N | N/A |
| `OPEN_TELEMETRY_TRACE_COLLECTOR_URLS` | N | N/A |
| `OUTGOING_PAYMENT_WORKER_IDLE` | N | The time, in milliseconds, that `OUTGOING_PAYMENT_WORKERS` wait until they check an empty outgoing payment request queue again. |
| `OUTGOING_PAYMENT_WORKERS` | N | The number of workers processing outgoing payment requests. |
| `POLL_INCOMING_PAYMENT_CREATED_WEBHOOK` | N | N/A |
| `PRIVATE_KEY_FILE` | N | The path to your Rafiki instance’s client private key. |
| `QUOTE_LIFESPAN` | N | The time, in milliseconds, an Open Payments quote is valid for. |
| `REDIS_TLS_CA_FILE_PATH` | N | [Redis TLS config](https://redis.io/docs/latest/operate/oss_and_stack/management/security/encryption/) |
| `REDIS_TLS_CERT_FILE_PATH` | N | [Redis TLS config](https://redis.io/docs/latest/operate/oss_and_stack/management/security/encryption/) |
| `REDIS_TLS_KEY_FILE_PATH` | N | [Redis TLS config](https://redis.io/docs/latest/operate/oss_and_stack/management/security/encryption/) |
| `SIGNATURE_SECRET` | N | The secret to generate request header signatures for webhook event requests. |
| `SIGNATURE_VERSION` | N | The version number to generate request header signatures for webhook events. |
| `SLIPPAGE` | N | The accepted ILP rate fluctuation. |
| `STREAM_SECRET` | N | The seed secret to generate shared STREAM secrets. |
| `TELEMETRY_EXCHANGE_RATES_LIFETIME` | N | N/A |
| `TELEMETRY_EXCHANGE_RATES_URL` | N | The endpoint Rafiki will query for exchange rates. Used as a fallback if/when [exchange rates](/integration/requirements/exchange-rates) aren’t provided. |
| `TIGERBEETLE_CLUSTER_ID` | N | The TigerBeetle cluster ID picked by the system that starts the TigerBeetle cluster to create a [TigerBeetle client](https://docs.tigerbeetle.com/clients/node/#creating-a-client). |
| `TIGERBEETLE_REPLICA_ADDRESSES` | N | TigerBeetle replica addresses for all replicas in the cluster. The addresses are comma-separated IP addresses/ports, to create a [TigerBeetle client](https://docs.tigerbeetle.com/clients/node/#creating-a-client). |
| `TIGERBEETLE_REPLICA_ADDRESSES.SPLIT` | N | N/A |
| `TIGERBEETLE_TWO_PHASE_TIMEOUT_SECONDS` | N | N/A |
| `TRUST_PROXY` | N | Must be set to `true` when running Rafiki behind a proxy. When `true`, the `X-Forwarded-Proto` header is used to determine if connections are secure. |
| `WALLET_ADDRESS_DEACTIVATION_PAYMENT_GRACE_PERIOD_MS` | N | The time into the future, in milliseconds, to set expiration of Open Payments incoming payments when deactivating a wallet address. |
| `WALLET_ADDRESS_LOOKUP_TIMEOUT_MS` | N | The time, in milliseconds, you have to create a missing wallet address before timeout. |
| `WALLET_ADDRESS_POLLING_FREQUENCY_MS` | N | The frequency of polling while waiting for you to create a missing wallet address. |
| `WALLET_ADDRESS_URL` | N | Your Rafiki instance’s internal wallet address. |
| `WALLET_ADDRESS_WORKER_IDLE` | N | The time, in milliseconds, that `WALLET_ADDRESS_WORKERS` wait until checking the empty wallet address request queue again. |
| `WALLET_ADDRESS_WORKERS` | N | The number of workers processing wallet address requests. |
| `WEBHOOK_MAX_RETRY` | N | The maximum number of times your Rafiki instance’s backend retries sending a certain webhook event to your configured `WEBHOOK_URL`. |
| `WEBHOOK_TIMEOUT` | N | The time, in milliseconds, that your Rafiki instance will wait for a `200` response from your webhook endpoint. If a `200` response isn’t received, Rafiki will time out and try to send the webhook event again. |
| `WEBHOOK_WORKER_IDLE` | N | The time, in milliseconds, that `WEBHOOK_WORKERS` will wait until they check the empty webhook event queue again. |
| `WEBHOOK_WORKERS` | N | The number of workers processing webhook events. |
| `WITHDRAWAL_THROTTLE_DELAY` | N | The delay in liquidity withdrawal processing. |
#### [Frontend service](#frontend-service)
[Section titled “Frontend service”](#frontend-service)
The Rafiki `frontend` service provides an internal admin interface for managing your Rafiki instance. It communicates with the Backend Admin API to facilitate administrative tasks. See [Frontend service](/integration/deployment/services/frontend-service) for more information.
Ports exposed:
* 3005 (`PORT`) is used to host the Rafiki Admin app
Make sure to configure the `GRAPHQL_URL` and `OPEN_PAYMENTS_URL` environment variables to point to the appropriate endpoints.
```sh
rafiki-frontend:
image: ghcr.io/interledger/rafiki-frontend:
container_name: rafiki-frontend
depends_on:
- rafiki-backend
environment:
PORT: 3005
GRAPHQL_URL: {https://myrafiki.com:3001}
OPEN_PAYMENTS_URL: {https://myrafiki.com}
KRATOS_CONTAINER_PUBLIC_URL: {http://kratos:4433}
KRATOS_BROWSER_PUBLIC_URL: {https://admin.myrafiki.com/kratos}
KRATOS_ADMIN_URL: {http://kratos:4434/admin}
networks:
- rafiki
restart: always
privileged: true
ports:
- '3005:3005'
```
##### [Environment variables](#environment-variables-2)
[Section titled “Environment variables”](#environment-variables-2)
| Variable | Required | Description |
| -------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GRAPHQL_URL` | Y | URL of Rafiki’s GraphQL Auth Admin API. |
| `OPEN_PAYMENTS_URL` | Y | Your Open Payments API endpoint. |
| `PORT` | Y | Port from which to host the Rafiki Remix app. |
| `AUTH_ENABLED` | N | When `true`, only authenticated users can be granted access to Rafiki Admin by an administrator. |
| `ENABLE_INSECURE_MESSAGE_COOKIE` | N | When set to `true`, `t`, or `1`, cookie will be transmitted over insecure HTTP connection. Insecure message cookies are required for flash messages to work over HTTP. |
| `KRATOS_ADMIN_URL` | N | The admin endpoint/container address for Kratos. |
| `KRATOS_BROWSER_PUBLIC_URL` | N | The URL for you to access the Kratos Docker container from a browser outside the Docker network. This is used for calls from a browser (what you see in the Rafiki Admin UI) to the Kratos server on the backend. |
| `KRATOS_CONTAINER_PUBLIC_URL` | N | The URL for you to access the Kratos Docker container from within the Docker network. This is used for backend calls to Kratos. |
| `LOG_LEVEL` | N | [Pino log level](https://getpino.io/#/docs/api?id=levels). |
| `NODE_ENV` | N | The type of node environment: `development`, `test`, or `production`. |
| `SIGNATURE_SECRET` | N | The signature secret used to authenticate requests to the Backend Admin API. |
| `SIGNATURE_VERSION` | N | The signature version number used to authenticate requests to the Backend Admin API. |
#### [TigerBeetle](#tigerbeetle)
[Section titled “TigerBeetle”](#tigerbeetle)
TigerBeetle is a high-performance database designed to handle double-entry/double-ledger accounting. It’s recommended for managing liquidity and settlement accounts due to its speed and efficiency. See [Accounting](/overview/concepts/accounting/#tigerbeetle) for more information.
To use TigerBeetle, make sure that `USE_TIGERBEETLE` is set to true in the backend service environment variables.
```sh
tigerbeetle:
image: ghcr.io/tigerbeetle/tigerbeetle:0.16.29
privileged: true
volumes:
- tigerbeetle-data:/var/lib/tigerbeetle
networks:
rafiki:
ipv4_address: 10.5.0.50
entrypoint:
- /bin/sh
- -c
- |
set -ex
DATA_FILE=/var/lib/tigerbeetle/cluster_0_replica_0.tigerbeetle
set +e
ls $$DATA_FILE
DATA_FILE_EXISTS="$$?"
set -e
echo $$DATA_FILE_EXISTS
if [ "$$DATA_FILE_EXISTS" != 0 ]; then
./tigerbeetle format --cluster=0 --replica=0 --replica-count=1 $$DATA_FILE;
fi
hostname -i
ls /var/lib/tigerbeetle
./tigerbeetle start --addresses=0.0.0.0:4342 $$DATA_FILE
```
#### [Postgres](#postgres)
[Section titled “Postgres”](#postgres)
The Postgres service is a relational database management system used to store and manage application data. Both the `auth` and `backend` services rely on Postgres databases.
```sh
postgres:
image: 'postgres:16'
container_name: postgres
environment:
POSTGRES_USER: ...
POSTGRES_PASSWORD: ...
networks:
- rafiki
restart: unless-stopped
volumes:
- pg-data:/var/lib/postgresql/data
- ../dbinit.sql:/docker-entrypoint-initdb.d/init.sql
```
#### [Redis](#redis)
[Section titled “Redis”](#redis)
The Redis service is used for caching and session management across the application. Both the `auth` and `backend` services rely on Redis databases.
```sh
redis:
image: 'redis:7'
restart: unless-stopped
networks:
- rafiki
```
#### [Kratos](#kratos)
[Section titled “Kratos”](#kratos)
The Kratos service is an identity and user management solution used by Rafiki’s `frontend` service for handling authentication and user management tasks.
Disabling authentication
Rafiki Admin provides access to sensitive data like peering relationships and wallet addresses. Authentication is enabled by default to restrict access to authorized users only.
In the [Local Playground](/integration/playground/overview#rafiki-admin), authentication is disabled by default to simplify local development and testing. To disable it in other environments, set the environment variable `AUTH_ENABLED` to `false`. This should be done with **extreme caution** and only in specific scenarios:
* Secure, non-production environments like the Local Playground for local development and testing.
* Internal systems where Rafiki Admin isn’t exposed externally and other access controls (firewalls, local-only access) ensure that the system is secured.
```sh
kratos:
image: 'oryd/kratos:v1.2.0'
privileged: true
ports:
- '4433:4433'
volumes:
- ../entrypoint.sh:/entrypoint.sh
- ../identity.schema.json:/etc/config/kratos/identity.schema.json
- ./kratos.yml:/etc/config/kratos/kratos.yml
entrypoint: ['/entrypoint.sh']
networks:
- rafiki
```
#### [Networks and volumes](#networks-and-volumes)
[Section titled “Networks and volumes”](#networks-and-volumes)
Networks and volumes are necessary in Docker Compose for enabling communication between services and persisting data storage for containers.
```sh
networks:
testnet:
driver: bridge
ipam:
config:
- subnet: 10.5.0.0/24
gateway: 10.5.0.1
volumes:
pg-data:
tigerbeetle-data:
```
#### [Complete Docker Compose example](#complete-docker-compose-example)
[Section titled “Complete Docker Compose example”](#complete-docker-compose-example)
Click to expand
```sh
name: 'my-rafiki'
services:
rafiki-auth:
image: ghcr.io/interledger/rafiki-auth:
container_name: rafiki-auth
environment:
AUTH_DATABASE_URL: {postgresql://...}
AUTH_SERVER_URL: {https://auth.myrafiki.com}
ADMIN_PORT: 3003
AUTH_PORT: 3006
INTROSPECTION_PORT: 3007
INTERACTION_PORT: 3009
SERVICE_API_PORT: 3011
COOKIE_KEY: {...}
IDENTITY_SERVER_SECRET: {...}
IDENTITY_SERVER_URL: {https://idp.mysystem.com}
REDIS_URL: {redis://...}
TRUST_PROXY: true
depends_on:
- postgres
networks:
- rafiki
ports:
- '3003:3003'
- '3006:3006'
- '3007:3007'
- '3009:3009'
- '3011:3011'
restart: always
rafiki-backend:
image: ghcr.io/interledger/rafiki-backend:
container_name: rafiki-backend
depends_on: - postgres - redis
environment:
AUTH_SERVER_GRANT_URL: {https://auth.myrafiki.com}
AUTH_SERVER_INTROSPECTION_URL: {https://auth.myrafiki.com/3007}
AUTH_SERVICE_API_URL: {https://auth.myrafiki.com/3011}
DATABASE_URL: {postgresql://...}
ILP_ADDRESS: {test.myrafiki}
ADMIN_PORT: 3001
CONNECTOR_PORT: 3002
OPEN_PAYMENTS_PORT: 3000
OPEN_PAYMENTS_URL: {https://myrafiki.com}
REDIS_URL: {redis://...}
WALLET_ADDRESS_URL: {https://myrafiki.com/rafiki-instance}
WEBHOOK_URL: {https://mysystem.com/webhooks}
EXCHANGE_RATES_URL: {https://mysystem.com/rates}
ILP_CONNECTOR_URL: {https://ilp.myrafiki.com}
INSTANCE_NAME: {'My ASE name'}
TRUST_PROXY: true
KEY_ID: ...
USE_TIGERBEETLE: true
TIGERBEETLE_CLUSTER_ID: 0
TIGERBEETLE_REPLICA_ADDRESSES: 10.5.0.50:4342
networks: - rafiki
ports: - '3000:3000' - '3001:3001' - '3002:3002'
privileged: true
restart: always
volumes: - ../temp/:/workspace/temp/
rafiki-frontend:
image: ghcr.io/interledger/rafiki-frontend:
container_name: rafiki-frontend
depends_on: - rafiki-backend
environment:
PORT: 3005
GRAPHQL_URL: {https://myrafiki.com:3001}
OPEN_PAYMENTS_URL: {https://myrafiki.com}
KRATOS_CONTAINER_PUBLIC_URL: {http://kratos:4433}
KRATOS_BROWSER_PUBLIC_URL: {https://admin.myrafiki.com/kratos}
KRATOS_ADMIN_URL: {http://kratos:4434/admin}
networks: - rafiki
restart: always
privileged: true
ports: - '3005:3005'
tigerbeetle:
image: ghcr.io/tigerbeetle/tigerbeetle:0.16.29
privileged: true
volumes: - tigerbeetle-data:/var/lib/tigerbeetle
networks:
rafiki:
ipv4_address: 10.5.0.50
entrypoint: - /bin/sh - -c - |
set -ex
DATA_FILE=/var/lib/tigerbeetle/cluster_0_replica_0.tigerbeetle
set +e
ls $$DATA_FILE
DATA_FILE_EXISTS="$$?"
set -e
echo $$DATA_FILE_EXISTS
if [ "$$DATA_FILE_EXISTS" != 0 ]; then
./tigerbeetle format --cluster=0 --replica=0 --replica-count=1 $$DATA_FILE;
fi
hostname -i
ls /var/lib/tigerbeetle
./tigerbeetle start --addresses=0.0.0.0:4342 $$DATA_FILE
postgres:
image: 'postgres:16'
container_name: postgres
environment:
POSTGRES_USER: ...
POSTGRES_PASSWORD: ...
networks: - rafiki
restart: unless-stopped
volumes: - pg-data:/var/lib/postgresql/data - ../dbinit.sql:/docker-entrypoint-initdb.d/init.sql
redis:
image: 'redis:7'
restart: unless-stopped
networks: - rafiki
kratos:
image: 'oryd/kratos:v1.2.0'
privileged: true
ports: - '4433:4433'
volumes: - ../entrypoint.sh:/entrypoint.sh - ../identity.schema.json:/etc/config/kratos/identity.schema.json - ./kratos.yml:/etc/config/kratos/kratos.yml
entrypoint: ['/entrypoint.sh']
networks: - rafiki
networks:
testnet:
driver: bridge
ipam:
config: - subnet: 10.5.0.0/24
gateway: 10.5.0.1
volumes:
pg-data:
tigerbeetle-data:
```
## [Create Nginx config files](#create-nginx-config-files)
[Section titled “Create Nginx config files”](#create-nginx-config-files)
Create Nginx configuration files for every exposed domain:
| Service | URL | Example | Nginx config file |
| ----------------------------- | ------------ | ------------------ | ------------------------------------------------------------------ |
| Open Payments resource server | DOMAIN | myrafiki.com | /etc/nginx/sites-available/open\_payments\_resource\_server.config |
| ILP connector | ilp.DOMAIN | ilp.myrafiki.com | /etc/nginx/sites-available/ilp.config |
| Open Payments auth server | auth.DOMAIN | auth.myrafiki.com | /etc/nginx/sites-available/open\_payments\_auth\_server.config |
| Admin UI | admin.DOMAIN | admin.myrafiki.com | /etc/nginx/sites-available/admin.config |
Note
The example domain and subdomain values are for demonstration purposes only. You must use the actual domain names that you set up with your DNS host.
### [Open Payments resource server (`backend` package)](#open-payments-resource-server-backend-package)
[Section titled “Open Payments resource server (backend package)”](#open-payments-resource-server-backend-package)
Using the editor of your choice, save the following file as `open_payments_resource_server.config` in the `/etc/nginx/sites-available` directory on your VM:
```sh
server {
server_name myrafiki.com;
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/myrafiki.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/myrafiki.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
proxy_http_version 1.1;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade "";
proxy_set_header Connection "";
proxy_set_header Host $server_name;
proxy_set_header Accept-Encoding "";
proxy_cache_bypass $http_upgrade;
proxy_pass_request_headers on;
proxy_pass http://localhost:3000;
}
}
server {
server_name myrafiki.com;
listen 80;
if ($host = myrafiki.com) {
return 301 https://$host$request_uri;
}
return 404;
}
```
### [ILP connector (`backend` package)](#ilp-connector-backend-package)
[Section titled “ILP connector (backend package)”](#ilp-connector-backend-package)
Save the following file as `ilp.config` in the `/etc/nginx/sites-available` directory on your VM:
```sh
server {
server_name ilp.myrafiki.com;
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/ilp.myrafiki.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ilp.myrafiki.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
proxy_http_version 1.1;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade "";
proxy_set_header Connection "";
proxy_set_header Host $server_name;
proxy_set_header Accept-Encoding "";
proxy_cache_bypass $http_upgrade;
proxy_pass_request_headers on;
proxy_pass http://localhost:3002;
}
}
server {
server_name ilp.myrafiki.com;
listen 80;
if ($host = ilp.myrafiki.com) {
return 301 https://$host$request_uri;
}
return 404;
}
```
### [Open Payments auth server (`auth` package)](#open-payments-auth-server-auth-package)
[Section titled “Open Payments auth server (auth package)”](#open-payments-auth-server-auth-package)
Save the following file as `open_payments_auth_server.config` in the `/etc/nginx/sites-available` directory on your VM:
```sh
server {
server_name auth.myrafiki.com;
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/auth.myrafiki.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/auth.myrafiki.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
proxy_http_version 1.1;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade "";
proxy_set_header Connection "";
proxy_set_header Host $server_name;
proxy_set_header Accept-Encoding "";
proxy_cache_bypass $http_upgrade;
proxy_pass_request_headers on;
proxy_pass http://localhost:3006;
}
}
server {
server_name auth.myrafiki.com;
listen 80;
if ($host = auth.myrafiki.com) {
return 301 https://$host$request_uri;
}
return 404;
}
```
### [Admin (`frontend` package)](#admin-frontend-package)
[Section titled “Admin (frontend package)”](#admin-frontend-package)
Save the following file as `admin.config` in the `/etc/nginx/sites-available` directory on your VM:
```sh
server {
server_name admin.myrafiki.com;
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/admin.myrafiki.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/admin.myrafiki.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
proxy_http_version 1.1;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade "";
proxy_set_header Connection "";
proxy_set_header Host $server_name;
proxy_set_header Accept-Encoding "";
proxy_cache_bypass $http_upgrade;
proxy_pass_request_headers on;
proxy_pass http://localhost:3005;
}
}
server {
server_name admin.myrafiki.com;
listen 80;
if ($host = admin.myrafiki.com) {
return 301 https://$host$request_uri;
}
return 404;
}
```
## [Set up symbolic links](#set-up-symbolic-links)
[Section titled “Set up symbolic links”](#set-up-symbolic-links)
Symbolic links allow Nginx to read your configuration files and redirect the local paths to the exposed domains and ports.
```sh
sudo ln -s /etc/nginx/sites-available/admin.conf /etc/nginx/sites-enabled/admin.conf
sudo ln -s /etc/nginx/sites-available/open_payments_auth_server.conf /etc/nginx/sites-enabled/open_payments_auth_server.conf
sudo ln -s /etc/nginx/sites-available/ilp.conf /etc/nginx/sites-enabled/ilp.conf
sudo ln -s /etc/nginx/sites-available/open_payments_resource_server.conf /etc/nginx/sites-enabled/open_payments_resource_server.conf
```
## [Deploy with Docker Compose](#deploy-with-docker-compose)
[Section titled “Deploy with Docker Compose”](#deploy-with-docker-compose)
Deploy the configured Rafiki services with Docker Compose:
```sh
docker compose up -d
```
# Helm & Kubernetes
This guide explains how to deploy Rafiki using Helm charts on a Kubernetes cluster. Helm is a package manager for Kubernetes that allows you to define, install, and upgrade complex Kubernetes applications through Helm charts.
Rafiki uses the following key infrastructure components:
* [PostgreSQL](https://www.postgresql.org/): Used for storing application data, grants, and Open Payments resources
* [Redis](https://redis.io/): Used for caching and session management
* [TigerBeetle](https://tigerbeetle.com/) (recommended): High-performance accounting database for financial transaction processing and ledger management
## [Prerequisites](#prerequisites)
[Section titled “Prerequisites”](#prerequisites)
Recommended software version
We recommend using the latest vendor supported version for each of the software dependencies listed in this section.
Before you begin, ensure you have the following:
* [Kubernetes](https://kubernetes.io/) cluster deployed
* [kubectl](https://kubernetes.io/docs/tasks/tools/) installed and configured to access your cluster
* [Helm](https://helm.sh/docs/intro/install/) installed
### [Deploy PostgreSQL and Redis dependencies](#deploy-postgresql-and-redis-dependencies)
[Section titled “Deploy PostgreSQL and Redis dependencies”](#deploy-postgresql-and-redis-dependencies)
Before installing Rafiki, ensure your target Kubernetes cluster already has Redis and PostgreSQL deployed.
* PostgreSQL must be initialized with two databases: one for the backend service and one for the auth service
* Each service must be configured with a connection URL that points to its corresponding database
## [Install Rafiki using Helm](#install-rafiki-using-helm)
[Section titled “Install Rafiki using Helm”](#install-rafiki-using-helm)
### [Add the Interledger Helm repository](#add-the-interledger-helm-repository)
[Section titled “Add the Interledger Helm repository”](#add-the-interledger-helm-repository)
Add the official Interledger Helm repository which contains the Rafiki charts:
```bash
helm repo add interledger-helm https://interledger.github.io/charts/interledger
helm repo update
```
### [Verify available charts](#verify-available-charts)
[Section titled “Verify available charts”](#verify-available-charts)
You can list the available Rafiki charts:
```bash
helm search repo interledger-helm/rafiki
```
This returns available chart versions, including the main `interledger-helm/rafiki` chart.
### [Create values file](#create-values-file)
[Section titled “Create values file”](#create-values-file)
Create a single `values.yaml` file to customize your Rafiki deployment.
Configure the following required values:
* `config.auth.redisUrl.value`
* `config.auth.databaseUrl.value`
* `config.backend.redisUrl.value`
* `config.backend.databaseUrl.value`
* `config.auth.tenancy.tenantId`
* `config.backend.tenancy.tenantId`
* `config.auth.identityServer.serverSecret.value`
* `config.backend.ilp.streamSecret.value`
* `config.backend.webhookSignatureSecret.value`
* `config.frontend.kratos.enabled`
Base values file
For full configuration details, refer to the base `values.yaml` file on GitHub: [interledger/charts `charts/interledger/rafiki/values.yaml`](https://github.com/interledger/charts/blob/main/charts/rafiki/values.yaml).
### [Configure chart-managed secrets](#configure-chart-managed-secrets)
[Section titled “Configure chart-managed secrets”](#configure-chart-managed-secrets)
When using `--set config.auth.shouldCreateSecrets=true` and `--set config.backend.shouldCreateSecrets=true`, Helm creates and manages the auth and backend Kubernetes secrets as part of the chart installation.
In this mode, include the required secret values in `values.yaml` so the chart can populate those generated secrets during install/upgrade:
```yaml
config:
auth:
identityServer:
serverSecret:
value: your-identity-server-secret
backend:
ilp:
streamSecret:
value: your-stream-secret
webhookSignatureSecret:
value: your-webhook-signature-secret
```
If you prefer to pre-create Kubernetes secrets manually, set both `config.auth.shouldCreateSecrets` and `config.backend.shouldCreateSecrets` to `false` and configure `secretKeyRef` values in `values.yaml`.
### [Install the service](#install-the-service)
[Section titled “Install the service”](#install-the-service)
Install Rafiki using the following command:
```bash
helm install rafiki interledger-helm/rafiki -f values.yaml \
--set config.auth.shouldCreateSecrets=true \
--set config.backend.shouldCreateSecrets=true
```
If you want to install to a specific namespace:
```bash
kubectl create namespace rafiki
helm install rafiki interledger-helm/rafiki -f values.yaml \
--set config.auth.shouldCreateSecrets=true \
--set config.backend.shouldCreateSecrets=true \
-n rafiki
```
The namespace used for Rafiki must match the namespace used by your Redis and PostgreSQL services.
### [Verify the deployment](#verify-the-deployment)
[Section titled “Verify the deployment”](#verify-the-deployment)
Check the status of your deployments:
```bash
# Check Helm releases
helm list
# Check running pods
kubectl get pods
# Check deployed services
kubectl get services
# Check logs for any issues
kubectl logs -l app.kubernetes.io/name=rafiki-auth
kubectl logs -l app.kubernetes.io/name=rafiki-backend
kubectl logs -l app.kubernetes.io/name=rafiki-frontend
```
## [Configure environment variables](#configure-environment-variables)
[Section titled “Configure environment variables”](#configure-environment-variables)
Each Rafiki service can be configured via environment variables through Helm values. Below are the complete environment variable configurations for each service:
Auth service
The Rafiki `auth` service is responsible for handling Open Payments authorization and grant management. It implements the Open Payments authorization server specification and manages access tokens, grants, and interactions. The auth service connects to a PostgreSQL database to store grant-related data and uses Redis for session management.
**Ports exposed:**
* `3003` (admin) - Auth Admin API for managing grants and access tokens
* `3006` (auth) - Open Payments authorization server endpoint
* `3007` (introspection) - Access token introspection endpoint
* `3009` (interaction) - User interaction endpoint for grant flows
Running Rafiki behind a proxy
If you plan to run your Rafiki instance behind a proxy, you must set the `trustProxy` variable to `true`
### [Required values](#required-values)
[Section titled “Required values”](#required-values)
| Helm variable name | Default | Description |
| ----------------------------------------------- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `config.auth.databaseUrl.value` | `''` | The URL of the PostgreSQL database storing your Open Payments grant data. |
| `config.auth.redisUrl.value` | `''` | The connection URL of Redis used for session/cache operations. |
| `config.auth.authServerUrl` | `http://rafiki-auth:3006` | The public endpoint for your Rafiki instance’s public Open Payments auth routes. |
| `config.auth.cookieKey.value` | `"changeme"` | The koa KeyGrip key that’s used to sign cookies for an interaction session. |
| `config.auth.identityServer.domain` | `http://rafiki-backend/idp` | The URL of your identity provider (IdP) server, used by the authorization server to tell an Open Payments client where to redirect the end-user to start interactions. |
| `config.auth.identityServer.serverSecret.value` | `''` | A shared secret between the authorization server and the identity provider (IdP); the authorization server uses the secret to secure its IdP-related endpoints. When the IdP sends requests to the authorization server, it must provide the secret via an `x-idp-secret` header. |
### [Conditionally required values](#conditionally-required-values)
[Section titled “Conditionally required values”](#conditionally-required-values)
| Helm value name | Default | Description |
| ------------------------------ | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `config.auth.trustProxy` | `true` | Must be set to `true` when running Rafiki behind a proxy. When `true`, the `X-Forwarded-Proto` header is used to determine if connections are secure. |
| `config.auth.tenancy.tenantId` | `some-guid` | The tenant identifier for the operator tenant. Should match `config.backend.tenancy.tenantId`. |
### [Optional values](#optional-values)
[Section titled “Optional values”](#optional-values)
| Helm value name | Default | Description |
| ----------------------------------------- | ------------------ | ---------------------------------------------------------------------------------------------------- |
| `config.auth.shouldCreateSecrets` | `false` | If `true`, chart-managed auth secrets are created from `secretsMaps.auth`. |
| `config.auth.accessToken.deletionDays` | `30` | The days until expired and/or revoked access tokens are deleted. |
| `config.auth.accessToken.expirySeconds` | `600` (10 minutes) | The expiry time, in seconds, for access tokens. |
| `config.auth.admin.signatureVersion` | `1` | The version of the request signing algorithm used to validate signatures. |
| `config.auth.admin.signatureTtlSeconds` | `30` | The TTL, in seconds, for which a request signature is valid. |
| `config.auth.port.admin` | `3003` | The port of your Rafiki Auth Admin API server. |
| `config.auth.port.auth` | `3006` | The port of your Open Payments authorization server. |
| `config.auth.port.interaction` | `3009` | The port of your Open Payments interaction-related API. |
| `config.auth.port.introspection` | `3007` | The port of your Open Payments access token introspection server. |
| `config.auth.workers.cleanup` | `1` | The number of workers processing expired or revoked access tokens. |
| `config.auth.interaction.incomingPayment` | `"false"` | When `"true"`, incoming Open Payments grant requests are interactive. |
| `config.auth.interaction.expirySeconds` | `600` (10 minutes) | The time, in seconds, for which a user can interact with a grant request before the request expires. |
| `config.auth.interaction.quote` | `"false"` | When `"true"`, quote grants are interactive. |
| `config.auth.logLevel` | `info` | Pino log level. |
| `config.auth.nodeEnv` | `production` | The type of node environment: `development`, `test`, or `production`. |
| `config.auth.grant.waitSeconds` | `5` | The wait time, in seconds, included in a grant request response (`grant.continue`). |
Backend service
The Rafiki `backend` service handles the core Open Payments capabilities and business logic. It exposes the Open Payments resource server APIs for wallet addresses, incoming payments, outgoing payments, and quotes. The backend also includes an ILP connector for sending and receiving Interledger packets. It connects to PostgreSQL for storing Open Payments resources, Redis for caching, and optionally TigerBeetle for high-performance accounting.
**Ports exposed:**
* `3000` (openPayments) - Open Payments resource server API
* `3001` (admin) - Backend Admin API and GraphQL endpoint
* `3002` (connector) - ILP connector for sending and receiving packets
* `3005` (autoPeering) - Autopeering service (when enabled)
TigerBeetle vs PostgreSQL for accounting
TigerBeetle is strongly recommended for production deployments due to its high-performance accounting capabilities. If you prefer to use PostgreSQL as the accounting database, set `useTigerbeetle` to `false`.
### [Required values](#required-values)
[Section titled “Required values”](#required-values)
| Helm value name | Default | Description |
| --------------------------------------------- | ------------------------------------- | -------------------------------------------------------------------------------------- |
| `config.backend.auth.grantUrl` | `http://rafiki-auth.rafiki-auth:3006` | The endpoint on your Open Payments authorization server to make a grant request. |
| `config.backend.auth.introspectionUrl` | `http://rafiki-auth.rafiki-auth:3007` | The endpoint on your Open Payments authorization server to introspect an access token. |
| `config.backend.databaseUrl.value` | `''` | The PostgreSQL database URL of the database storing your resource data. |
| `config.backend.redisUrl.value` | `''` | The Redis URL used for queue, cache, and connector state. |
| `config.backend.ilp.host` | `http://rafiki-backend:3000` | The public endpoint of your Open Payments resource server. |
| `config.backend.ilp.address` | `test.rafiki-backend` | The ILP address of your Rafiki instance. |
| `config.backend.ilp.connector` | `http://rafiki-backend:3002` | The ILP connector address where ILP packets are received. |
| `config.backend.key.id` | `rafiki-override-this-value` | Your Rafiki instance’s client key ID. |
| `config.backend.webhook.url` | `http://wallet/webhooks/rafiki` | Your endpoint that consumes webhook events. |
| `config.backend.ilp.streamSecret.value` | `''` | The seed secret to generate shared STREAM secrets. |
| `config.backend.webhookSignatureSecret.value` | `''` | The secret used to generate webhook request signatures. |
### [Conditionally required values](#conditionally-required-values)
[Section titled “Conditionally required values”](#conditionally-required-values)
| Helm value name | Default | Description |
| --------------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `config.backend.instanceName` | `rafiki-backend-changeme` | Your Rafiki instance name used for automatic peering and/or telemetry. Required when either feature is enabled. |
| `config.backend.trustProxy` | `"true"` | Must be set to `true` when running Rafiki behind a proxy. When `true`, the `X-Forwarded-Proto` header is used to determine if connections are secure. |
| `config.backend.tenancy.tenantId` | `some-guid` | The tenant identifier for the operator tenant. Should match `config.auth.tenancy.tenantId`. |
### [Optional values](#optional-values)
[Section titled “Optional values”](#optional-values)
| Helm value name | Default | Description |
| ---------------------------------------- | ------------------- | ---------------------------------------------------------------------------------------------------- |
| `config.backend.shouldCreateSecrets` | `false` | If `true`, chart-managed backend secrets are created from `secretsMaps.backend`. |
| `config.backend.port.admin` | `3001` | The port of your Backend Admin API server. |
| `config.backend.port.connector` | `3002` | The port of the ILP connector for sending packets via ILP over HTTP. |
| `config.backend.port.openPayments` | `3000` | The port of your Open Payments resource server. |
| `config.backend.port.autoPeering` | `3005` | If automatic peering is enabled, the server uses this port. |
| `config.backend.autoPeering.enabled` | `"false"` | When `"true"`, automatic peering is enabled. |
| `config.backend.telemetry.enabled` | `"false"` | When `"true"`, telemetry is enabled. |
| `config.backend.useTigerbeetle` | `false` | When `true`, TigerBeetle is used for accounting. |
| `config.backend.rates.url` | `''` | The endpoint your Rafiki instance uses to request exchange rates. |
| `config.backend.webhook.timeout` | `200` | The time, in milliseconds, for wallet-address worker webhook polling timeout behavior. |
| `config.backend.ilp.slippage` | `0.01` | The accepted ILP rate fluctuation. |
| `config.backend.key.mount` | `/mnt/keys/pvk.pem` | The mounted path to your Rafiki instance private key file. |
| `config.backend.idempotency.keyTTL` | `86400000` | The TTL, in milliseconds, for idempotency keys on Backend Admin API mutations. |
| `config.backend.idempotency.keyLock` | `2000` | The TTL, in milliseconds, for idempotency key lock concurrency control. |
| `config.backend.workerIdle` | `200` | The time, in milliseconds, workers wait before polling an empty queue again. |
| `config.backend.workers.incomingPayment` | `1` | The number of workers processing incoming payment requests. |
| `config.backend.workers.outgoingPayment` | `1` | The number of workers processing outgoing payment requests. |
| `config.backend.workers.walletAddress` | `1` | The number of workers processing wallet address requests. |
| `config.backend.workers.webhook` | `1` | The number of workers processing webhook events. |
| `config.backend.lifetime.exchangeRate` | `15000` | The time, in milliseconds, that exchange rates from `config.backend.rates.url` are considered valid. |
| `config.backend.lifetime.quote` | `300000` | The time, in milliseconds, an Open Payments quote is valid for. |
| `config.backend.lifetime.webhook` | `200` | The time, in milliseconds, to wait for a `200` response from your webhook endpoint before retry. |
| `config.backend.withdrawalThrottleDelay` | `''` | The delay in liquidity withdrawal processing. |
| `config.backend.logLevel` | `info` | Pino log level. |
| `config.backend.nodeEnv` | `production` | The type of node environment: `development`, `test`, or `production`. |
Frontend service
The Rafiki `frontend` service provides an administrative web interface for managing your Rafiki instance. It offers a user-friendly dashboard for monitoring and managing wallet addresses, payments, peers, and other Open Payments resources. The frontend communicates with the Backend Admin API to perform administrative operations.
**Ports exposed:**
* `3010` (http) - Rafiki Admin web interface
Authentication
The frontend can optionally integrate with Ory Kratos for user authentication. When authentication is disabled (`kratos.enabled: false`), the admin interface is accessible without login credentials.
#### [Required Values](#required-values)
[Section titled “Required Values”](#required-values)
| Helm value name | Default | Description |
| ----------------------------------------------- | --------------------------------------------------- | -------------------------------------------- |
| `config.frontend.serviceUrls.GRAPHQL_URL` | `http://rafiki-backend-service.rafiki:3001/graphql` | URL of Rafiki’s GraphQL Admin API |
| `config.frontend.serviceUrls.OPEN_PAYMENTS_URL` | `https://rafiki-backend-service/` | Your Open Payments API endpoint |
| `config.frontend.port` | `3010` | Port from which to host the Rafiki Remix app |
#### [Conditionally required values](#conditionally-required-values)
[Section titled “Conditionally required values”](#conditionally-required-values)
The following values are required only when `config.frontend.kratos.enabled` is set to `true`.
| Helm value name | Default | Description |
| ------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------- |
| `config.frontend.kratos.enabled` | `true` | Enables Kratos-backed authentication for the frontend. |
| `config.frontend.kratos.adminUrl` | *undefined* | The admin endpoint/container address for Kratos. |
| `config.frontend.kratos.containerPublicUrl` | *undefined* | The URL for internal backend calls to the Kratos container from within the cluster network. |
| `config.frontend.kratos.browserPublicUrl` | *undefined* | The browser-facing URL for Kratos, used by the Rafiki Admin UI to complete auth flows. |
#### [Optional values](#optional-values)
[Section titled “Optional values”](#optional-values)
| Helm value name | Default | Description |
| ---------------------------------- | ------------ | ---------------------------------------------------------------------------------------- |
| `config.frontend.signatureVersion` | `1` | The signature version number used for outgoing HTTP signatures to the Backend Admin API. |
| `config.frontend.nodeEnv` | `production` | The type of node environment: `development`, `test`, or `production`. |
| `config.frontend.logLevel` | `info` | Pino log level. |
## [Configure ingress](#configure-ingress)
[Section titled “Configure ingress”](#configure-ingress)
To expose Rafiki services outside the cluster, you’ll need to configure ingress. Here’s an example using NGINX Ingress Controller:
### [Install NGINX Ingress Controller](#install-nginx-ingress-controller)
[Section titled “Install NGINX Ingress Controller”](#install-nginx-ingress-controller)
If you don’t already have an ingress controller installed:
```bash
# Add the ingress-nginx repository
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
# Install the ingress controller
helm install nginx-ingress ingress-nginx/ingress-nginx \
--set controller.publishService.enabled=true
```
### [Configure ingress in your values file](#configure-ingress-in-your-values-file)
[Section titled “Configure ingress in your values file”](#configure-ingress-in-your-values-file)
Add ingress configuration to your `values.yaml` file:
```yaml
# In values.yaml
ingress:
backend:
enabled: true
className: nginx
hosts:
- host: rafiki-backend.your-domain.com
paths:
- path: /
pathType: Prefix
service:
name: rafiki-backend-service
port: 3000
tls:
- secretName: rafiki-backend-tls
hosts:
- rafiki-backend.your-domain.com
auth:
enabled: true
className: nginx
hosts:
- host: rafiki-auth.your-domain.com
paths:
- path: /
pathType: Prefix
service:
name: rafiki-auth-service
port: 3006
tls:
- secretName: rafiki-auth-tls
hosts:
- rafiki-auth.your-domain.com
```
### [Configure DNS](#configure-dns)
[Section titled “Configure DNS”](#configure-dns)
Create DNS records pointing to your ingress controller’s external IP:
* `rafiki-backend.your-domain.com`
* `rafiki-auth.your-domain.com`
* `rafiki-frontend.your-domain.com`
## [Configure dependencies](#configure-dependencies)
[Section titled “Configure dependencies”](#configure-dependencies)
### [PostgreSQL](#postgresql)
[Section titled “PostgreSQL”](#postgresql)
You can use an external PostgreSQL instance or deploy one using Helm:
```bash
# Add Bitnami repository
helm repo add bitnami https://charts.bitnami.com/bitnami
# Install PostgreSQL for the backend database
helm install rafiki-postgres bitnami/postgresql \
--set auth.postgresPassword=your-secure-password \
--set auth.database=rafiki_backend \
--set primary.persistence.size=20Gi
```
Then create the auth database:
```bash
kubectl exec -it rafiki-postgres-postgresql-0 -- psql -U postgres -d postgres -c "CREATE DATABASE rafiki_auth;"
```
### [Redis](#redis)
[Section titled “Redis”](#redis)
Deploy Redis for caching and session management:
```bash
# Install Redis
helm install rafiki-redis bitnami/redis \
--set auth.enabled=false \
--set master.persistence.size=8Gi
```
### [TigerBeetle (recommended)](#tigerbeetle-recommended)
[Section titled “TigerBeetle (recommended)”](#tigerbeetle-recommended)
For high-performance accounting, deploy TigerBeetle:
```bash
# Add TigerBeetle repository
helm repo add tigerbeetle https://tigerbeetle.github.io/helm-charts
# Install TigerBeetle
helm install rafiki-tigerbeetle tigerbeetle/tigerbeetle \
--set replicaCount=3 \
--set persistence.size=20Gi
```
## [Upgrade Rafiki](#upgrade-rafiki)
[Section titled “Upgrade Rafiki”](#upgrade-rafiki)
To upgrade your Rafiki services to newer versions:
```bash
helm repo update
helm upgrade rafiki interledger-helm/rafiki -f values.yaml
```
## [Uninstall Rafiki](#uninstall-rafiki)
[Section titled “Uninstall Rafiki”](#uninstall-rafiki)
To uninstall Rafiki services:
```bash
helm uninstall rafiki
# Optionally uninstall dependencies
helm uninstall rafiki-postgres
helm uninstall rafiki-redis
```
Persistent data
Uninstalling doesn’t delete Persistent Volume Claims (PVCs) by default. To delete all data:
```bash
kubectl delete pvc -l app.kubernetes.io/instance=rafiki-postgres
kubectl delete pvc -l app.kubernetes.io/instance=rafiki-redis
kubectl delete pvc -l app.kubernetes.io/instance=rafiki-tigerbeetle
```
## [Troubleshooting](#troubleshooting)
[Section titled “Troubleshooting”](#troubleshooting)
### [Check service health](#check-service-health)
[Section titled “Check service health”](#check-service-health)
Verify that all services are running correctly:
```bash
# Check pod status
kubectl get pods -l app.kubernetes.io/name=rafiki-auth
kubectl get pods -l app.kubernetes.io/name=rafiki-backend
kubectl get pods -l app.kubernetes.io/name=rafiki-frontend
# Check service endpoints
kubectl get endpoints
# Check for events
kubectl get events --sort-by=.metadata.creationTimestamp
```
### [Common issues and solutions](#common-issues-and-solutions)
[Section titled “Common issues and solutions”](#common-issues-and-solutions)
#### [Database connection errors](#database-connection-errors)
[Section titled “Database connection errors”](#database-connection-errors)
1. **Verify database connectivity:**
```bash
kubectl logs -l app.kubernetes.io/name=rafiki-backend | grep -i database
```
2. **Check PostgreSQL status:**
```bash
kubectl get pods -l app.kubernetes.io/name=postgresql
kubectl logs -l app.kubernetes.io/name=postgresql
```
3. **Test database connection:**
```bash
kubectl run -it --rm debug --image=postgres:14 -- psql postgresql://username:password@rafiki-postgres-postgresql:5432/rafiki_backend
```
#### [Redis connection issues](#redis-connection-issues)
[Section titled “Redis connection issues”](#redis-connection-issues)
1. **Check Redis status:**
```bash
kubectl get pods -l app.kubernetes.io/name=redis
kubectl logs -l app.kubernetes.io/name=redis
```
2. **Test Redis connectivity:**
```bash
kubectl run -it --rm debug --image=redis:7 -- redis-cli -h rafiki-redis ping
```
#### [Service discovery problems](#service-discovery-problems)
[Section titled “Service discovery problems”](#service-discovery-problems)
1. **Verify service names and ports:**
```bash
kubectl get services
kubectl describe service rafiki-backend-service
```
2. **Check DNS resolution:**
```bash
kubectl run -it --rm debug --image=busybox -- nslookup rafiki-backend-service
```
#### [Authentication and authorization issues](#authentication-and-authorization-issues)
[Section titled “Authentication and authorization issues”](#authentication-and-authorization-issues)
1. **Check auth service logs:**
```bash
kubectl logs -l app.kubernetes.io/name=rafiki-auth
```
2. **Verify secrets are correctly mounted:**
```bash
kubectl describe pod -l app.kubernetes.io/name=rafiki-auth
```
3. **Test auth endpoints:**
```bash
kubectl port-forward svc/rafiki-auth-service 3007:3007
curl http://localhost:3007/healthz
```
#### [Ingress and networking issues](#ingress-and-networking-issues)
[Section titled “Ingress and networking issues”](#ingress-and-networking-issues)
1. **Check ingress controller status:**
```bash
kubectl get pods -n ingress-nginx
kubectl logs -n ingress-nginx -l app.kubernetes.io/component=controller
```
2. **Verify ingress resources:**
```bash
kubectl get ingress
kubectl describe ingress rafiki-backend-ingress
```
3. **Check TLS certificates:**
```bash
kubectl get secrets
kubectl describe secret rafiki-backend-tls
```
### [Resource and performance issues](#resource-and-performance-issues)
[Section titled “Resource and performance issues”](#resource-and-performance-issues)
1. **Check resource usage:**
```bash
kubectl top pods
kubectl top nodes
```
2. **Monitor pod metrics:**
```bash
kubectl describe pod -l app.kubernetes.io/name=rafiki-backend
```
3. **Scale services if needed:**
```bash
# Update replicaCount in values.yaml and upgrade
helm upgrade rafiki interledger-helm/rafiki -f values.yaml
```
## [Production considerations](#production-considerations)
[Section titled “Production considerations”](#production-considerations)
### [Security](#security)
[Section titled “Security”](#security)
* **Use strong passwords**: Replace all default passwords with cryptographically secure values
* **Enable TLS**: Configure HTTPS for all external communications
* **Network policies**: Implement Kubernetes network policies to restrict pod-to-pod communication
* **RBAC**: Use Kubernetes Role-Based Access Control to limit cluster access
* **Secrets management**: Consider using external secret management solutions like HashiCorp Vault
* **Image security**: Use specific image tags and scan images for vulnerabilities
* **Pod security**: Configure pod security standards and security contexts
### [High availability](#high-availability)
[Section titled “High availability”](#high-availability)
* **Multi-replica deployments**: Run multiple replicas of critical services
* **Pod disruption budgets**: Configure disruption budgets to maintain availability during updates
* **Resource limits**: Set appropriate CPU and memory limits
* **Health checks**: Configure proper readiness and liveness probes
* **Load balancing**: Use multiple ingress controller replicas
### [Monitoring and observability](#monitoring-and-observability)
[Section titled “Monitoring and observability”](#monitoring-and-observability)
* **Prometheus**: Deploy Prometheus for metrics collection
* **Grafana**: Set up Grafana dashboards for visualization
* **Logging**: Implement centralized logging with ELK stack or similar
* **Alerting**: Configure alerting rules for critical issues
* **Distributed tracing**: Consider implementing distributed tracing for request flow analysis
### [Backup and disaster recovery](#backup-and-disaster-recovery)
[Section titled “Backup and disaster recovery”](#backup-and-disaster-recovery)
#### [Database backups](#database-backups)
[Section titled “Database backups”](#database-backups)
**PostgreSQL backup:**
```bash
# Create a backup job
kubectl create job --from=cronjob/postgres-backup manual-backup-$(date +%Y%m%d-%H%M%S)
# Manual backup
kubectl exec -it rafiki-postgres-postgresql-0 -- pg_dump -U postgres rafiki_backend > backup.sql
```
**TigerBeetle backup:**
```bash
# Create volume snapshots
kubectl create -f - <](/integration/deployment/services/auth-service)
Backend service
The Backend service hosts the Open Payments resource server, ILP connector, and Backend Admin API.
[Learn more >](/integration/deployment/services/backend-service)
Frontend service
The Frontend service offers an optional administrative user interface for interacting with the Backend Admin API.
[Learn more >](/integration/deployment/services/frontend-service)
## [Integration steps](#integration-steps)
[Section titled “Integration steps”](#integration-steps)
Integration checklist
Review the integration checklist for more required and optional steps.
[Review the checklist >](/integration/requirements/overview)
Tenants
Required A tenant represents an isolated environment for an ASE. You must create a tenant even if you don’t intend to share your Rafiki instance across ASEs.
[Create a tenant >](/integration/requirements/tenants)
Assets
Required An asset is a monetary unit represented by a currency code and a scale. Rafiki must be set up for at least one asset.
[Create an asset >](/integration/requirements/assets)
Wallet addresses
Required Each payment account in the ASE’s system must be linked to a wallet address. You must have at least one asset in Rafiki before creating wallet addresses.
[Create a wallet address >](/integration/requirements/wallet-addresses)
Webhook endpoint
Required You must expose a webhook endpoint that listens for events dispatched by Rafiki, then react accordingly by calling/interfacing with the Backend Admin API. For example, deposit or withdraw liquidity.
[Specify a webhook endpoint >](/integration/requirements/webhook-events)
## [Payments](#payments)
[Section titled “Payments”](#payments)
The following steps illustrate how to make a basic payment between two wallet addresses on the same Rafiki instance.
1\. Create an incoming payment
Use the Backend Admin API’s `createIncomingPayment` to create an incoming payment resource on the recipient’s wallet account.
[createIncomingPayment mutation >](https://rafiki.dev/apis/graphql/backend#mutation-createIncomingPayment)
2\. Create a quote
Use the Backend Admin API’s `createQuote` to create a quote resource on the sender’s wallet account. The quote shows how much it will cost the sender to deliver an amount to the receiver.
[createQuote mutation >](https://rafiki.dev/apis/graphql/backend#mutation-createQuote)
3\. Create an outgoing payment
Use the Backend Admin API’s `createOutgoingPayment` to create an outgoing payment resource on the sender’s wallet account. This operations starts the payment. At this point, the sender’s ASE must fund/approve the payment before it sends.
[createOutgoingPayment mutation >](https://rafiki.dev/apis/graphql/backend#mutation-createOutgoingPayment)
Handle webhook requests
As the payment flow progresses, the ASE is be notified about events that happen in the system. Some events are actionable, such as an `outgoing_payment.created` event. Review the webhook events page to learn more about handling each event.
[Webhook events >](/integration/requirements/webhook-events)
# Auto-Peering with the Test Network
You can start one local instance of Rafiki and peer it automatically with the remote [Test Network](https://wallet.interledger-test.dev/) by running the following commands:
```sh
## using Tigerbeetle DB
pnpm localenv:compose:autopeer
## OR using Postgres DB`
pnpm localenv:compose:psql:autopeer
```
The mock account servicing entity, Cloud Nine Wallet, in your local Rafiki instance will automatically peer with the remote [Test Network](https://wallet.interledger-test.dev/) instance. The required services will be exposed externally using the [localtunnel package](https://www.npmjs.com/package/localtunnel).
The exposed ports are:
| Service | Port |
| ------------- | ---- |
| Open Payments | 3000 |
| ILP connector | 3002 |
| Auth server | 3006 |
To use the Open Payments example in the [Bruno API collection](https://rafiki.dev/integration/playground/overview#bruno--open-payments-apis), follow these steps:
1. Navigate to [localhost:3030](http://localhost:3030) to find the list of created wallet addresses. Alternatively, you can run the following command:
```sh
docker logs rafiki-cloud-nine-mock-ase-1
```
2. Copy the URL of one of the wallet addresses.
3. Set the URL as the `senderWalletAddress` variable in the Bruno Autopeering environment.
Note
To visit the consent screen for the outgoing payment grant request, you must go through an additional login step by providing your IPv4 address as a tunnel password. You can look up your current IPv4 address by visiting [loca.lt/mytunnelpassword](https://loca.lt/mytunnelpassword) or [whatismyip.com](https://www.whatismyip.com/)
To shut down the connection and clear the environment, run the following command:
```sh
pnpm localenv:compose down
## Running the command to shut down is necessary
## as any subsequent script run to spin up a local Rafiki instance
## will use different wallet addresses.
```
# Overview
The Local Playground provides a suite of packages that, together, mock an account servicing entity that has deployed Rafiki. It exposes an [SPSP](https://interledger.org/developers/rfcs/simple-payment-setup-protocol/) endpoint, the [Open Payments APIs](/overview/concepts/open-payments) with its required [GNAP](https://datatracker.ietf.org/doc/draft-ietf-gnap-core-protocol/) auth endpoints to request grants, a STREAM endpoint for receiving Interledger packets, and the Rafiki Admin app to view and manage each Rafiki instance.
This suite of packages includes:
| Package name | Services |
| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| [`backend`](/integration/deployment/services/backend-service) | * SPSP
* Open Payments APIs
* GraphQL Admin APIs
* STREAM endpoint |
| [`auth`](/integration/deployment/services/auth-service) | GNAP auth server |
| `mock-account-servicing-entity` | mocks an account servicing entity |
| [`frontend`](/integration/deployment/services/frontend-service) | Remix app to expose a UI for Rafiki admin management via interaction with the Backend Admin APIs |
These packages depend on the following databases:

The Local Playground comes with containerized versions of the Rafiki packages and three pre-configured docker-compose files. [Cloud Nine Wallet](https://github.com/interledger/rafiki/blob/main/localenv/cloud-nine-wallet/docker-compose.yml) and [Happy Life Bank](https://github.com/interledger/rafiki/blob/main/localenv/happy-life-bank/docker-compose.yml) will start two mock account servicing entities with their respective Rafiki `backend` and `auth` servers. They automatically peer, and two to three user accounts are created on both of them. The third file is for [Cloud Ten Wallet](https://github.com/interledger/rafiki/blob/main/localenv/cloud-ten-wallet/docker-compose.yml) which is a mock ASE representing a tenant in a multi-tenant environment. See [Enabling multi-tenancy](#enabling-multi-tenancy) for more information.
This environment will set up a playground where you can use the GraphQL Admin APIs and the Open Payments APIs.
Note
The Mock ASE provided in this repository is intended solely for internal use and demonstration purposes. It’s not designed to serve as a reference architecture. If you are looking for a reference implementation of an ASE, please refer to the [Test Wallet](https://wallet.interledger-test.dev/).
## [Running the local environment](#running-the-local-environment)
[Section titled “Running the local environment”](#running-the-local-environment)
To spin up the Local Playground install the following software on your machine:
* A [Rafiki local environment setup](https://github.com/interledger/rafiki/blob/main/README.md#environment-setup)
* The [Docker](https://docs.docker.com/engine/install/) platform.
* An open-source API client [Bruno](https://www.usebruno.com/downloads)
### [Setup](#setup)
[Section titled “Setup”](#setup)
The local environment can be set up using either TigerBeetle or Postgres as the accounting database.
#### [Using TigerBeetle](#using-tigerbeetle)
[Section titled “Using TigerBeetle”](#using-tigerbeetle)
This option enables the primary instance (Cloud Nine Wallet) to use TigerBeetle for its accounting database. The secondary instance (Happy Life Bank) will always run Postgres as its accounting database.
To run the local environment with TigerBeetle, execute the following command from the root of the project:
Using TigerBeetle
```bash
pnpm localenv:compose up
```
#### [Using Postgres](#using-postgres)
[Section titled “Using Postgres”](#using-postgres)
If you want the primary instance (Cloud Nine Wallet) to use Postgres as the accounting database instead of TigerBeetle, you must use the `psql` variant of the `localenv:compose` command as follows:
Using Postgres
```bash
pnpm localenv:compose:psql up
```
#### [Enabling multi-tenancy](#enabling-multi-tenancy)
[Section titled “Enabling multi-tenancy”](#enabling-multi-tenancy)
The Local Playground can be configured to simulate a multi-tenant environment. In this configuration, Cloud Nine Wallet and Happy Life Bank are configured as separate operators, while Cloud Ten Wallet is configured as a tenant of Cloud Nine Wallet.
To run the local environment with multi-tenancy enabled, execute the following command from the root of the project:
Enabling multi-tenancy
```bash
pnpm localenv:compose:multitenancy up
```
### [Environment components](#environment-components)
[Section titled “Environment components”](#environment-components)
The local environment consists of a primary and secondary Rafiki instance, each with its docker-compose file ([Cloud Nine Wallet](https://github.com/interledger/rafiki/blob/main/localenv/cloud-nine-wallet/docker-compose.yml), [Happy Life Bank](https://github.com/interledger/rafiki/blob/main/localenv/cloud-nine-wallet/docker-compose.yml)). The primary Cloud Nine Wallet docker-compose file (`./cloud-nine-wallet/docker-compose.yml`) includes the primary Rafiki `backend` and `auth` services, and the required data stores, which include TigerBeetle (if enabled), Redis, and Postgres. The primary instance contains all the necessary components so that it can run independently.
The secondary Happy Life Bank docker-compose file (`./happy-life-bank/docker-compose.yml`) includes only the Rafiki services, not the data stores. It uses the data stores created by the primary Rafiki instance, so it can’t be run independently. The `pnpm localenv:compose up` command starts both the primary and secondary instances.
The following components are made available via the Local Playground:
[View full image](/img/localenv.png)
#### [Mock account servicing entity 1 - Cloud Nine Wallet](#mock-account-servicing-entity-1---cloud-nine-wallet)
[Section titled “Mock account servicing entity 1 - Cloud Nine Wallet”](#mock-account-servicing-entity-1---cloud-nine-wallet)
| Label | Component | URL |
| ----- | ---------------------------------- | ------------------------------- |
| a | User Interface | `http://localhost:3030` |
| b | Backend Admin API | `http://localhost:3001/graphql` |
| c | Open Payments API | `http://localhost:3000` |
| d | Auth Admin API | `http://localhost:3003/graphql` |
| e | Open Payments Auth API | `http://localhost:3006` |
| f | Rafiki Admin UI | `http://localhost:3010` |
| g | Kratos API - *disabled by default* | `http://localhost:4433` |
#### [Mock account servicing entity 2 - Happy Life Bank](#mock-account-servicing-entity-2---happy-life-bank)
[Section titled “Mock account servicing entity 2 - Happy Life Bank”](#mock-account-servicing-entity-2---happy-life-bank)
| Label | Component | URL |
| ----- | ---------------------------------- | ------------------------------- |
| h | User Interface | `http://localhost:3031` |
| i | Backend Admin API | `http://localhost:4001/graphql` |
| j | Open Payments API | `http://localhost:4000` |
| k | Auth Admin API | `http://localhost:4003/graphql` |
| l | Open Payments Auth API | `http://localhost:4006` |
| m | Rafiki Admin UI | `http://localhost:4010` |
| n | Kratos API - *disabled by default* | `http://localhost:4432` |
#### [MailSlurper](#mailslurper)
[Section titled “MailSlurper”](#mailslurper)
| Label | Component | URL |
| ----- | ------------------------------- | ----------------------- |
| o | Mail UI - *disabled by default* | `http://localhost:4436` |
#### [Database](#database)
[Section titled “Database”](#database)
| Component | URL |
| --------------- | ----------------------- |
| Postgres Server | `http://localhost:5432` |
### [Rafiki Admin](#rafiki-admin)
[Section titled “Rafiki Admin”](#rafiki-admin)
Manage and view information about the Rafiki instances through the [Rafiki Admin](/admin/admin-user-guide/) app. Rafiki Admin is a Remix app for querying info and executing mutations against the Rafiki [Backend Admin API](https://rafiki.dev/integration/playground/overview#admin-apis).
* Cloud Nine Wallet - `http://localhost:3010`
* Happy Life Bank - `http://localhost:4010`
We’ve secured access to Rafiki Admin using [Ory Kratos](https://www.ory.sh/docs/kratos/ory-kratos-intro); however, in our local playground setup we’ve chosen to disable user authentication for easier development and testing interactions.
Enabling Rafiki Admin authentication locally
If you’d like to enable authentication locally you can run `pnpm localenv:compose:adminauth up`
Separate registrations/users are required for each mock ASE’s Admin app as the ASEs are designed to run as separate mock entities. Visit the Rafiki Admin user guide to learn how to [invite](/admin/admin-user-guide#invite-a-user) and [remove](/admin/admin-user-guide#remove-a-user) users via provided scripts.
After you’ve registered, you can come back to your Rafiki Admin account by navigating to `localhost:3010` (Cloud Nine Wallet) or `localhost:4010` (Happy Life Bank) and logging in.
Follow these steps to reset a user’s Rafiki Admin password.
1. Select the forgot password link and enter an email for a registered user.
2. Open [Mail Slurper](http://localhost:4436/) to access the recovery link for the account.
### [Exploring accounts on Cloud Nine Wallet](#exploring-accounts-on-cloud-nine-wallet)
[Section titled “Exploring accounts on Cloud Nine Wallet”](#exploring-accounts-on-cloud-nine-wallet)
Navigate to `localhost:3030` to view the accounts on Cloud Nine Wallet.
Note
The accounts for Happy Life Bank can be found on `localhost:3031`.
Select an account name to view a list of transactions.
### [Debugging](#debugging)
[Section titled “Debugging”](#debugging)
Debuggers for the services are exposed on the following ports:
| Services | IP and Port |
| ----------------------- | -------------- |
| Cloud Nine Backend | 127.0.0.1:9229 |
| Cloud Nine Auth | 127.0.0.1.9230 |
| Happy Life Bank Backend | 127.0.0.1:9231 |
| Happy Life Bank Auth | 127.0.0.1:9232 |
#### [Debugging with a Chromium browser:](#debugging-with-a-chromium-browser)
[Section titled “Debugging with a Chromium browser:”](#debugging-with-a-chromium-browser)
1. Go to chrome://inspect
2. Select **Configure** and add the IP addresses and ports detailed above
3. Start the docker containers
4. Select **Inspect** on the service you want to debug to open the Chromium debugger.
You can trigger the debugger by adding `debugger` statements in the code and restarting the Docker containers or by adding breakpoints directly to the Chromium debugger after starting the Docker containers.
#### [Debugging with VS Code:](#debugging-with-vs-code)
[Section titled “Debugging with VS Code:”](#debugging-with-vs-code)
To debug with VS Code, add this configuration to your `.vscode/launch.json`:
```json
{
"name": "Attach to docker (cloud-nine-backend)",
"type": "node",
"request": "attach",
"port": 9229,
"address": "localhost",
"localRoot": "${workspaceFolder}",
"remoteRoot": "/home/rafiki/",
"restart": true
},
```
The `localRoot` variable will depend on the location of the `launch.json` file relative to Rafiki’s root directory.
For more ways to connect debuggers, refer to the [Node JS docs](https://nodejs.org/en/learn/getting-started/debugging) for debugging.
### [Shutting down](#shutting-down)
[Section titled “Shutting down”](#shutting-down)
To shut down your local instance, run the following commands:
```sh
# tear down
pnpm localenv:compose down
# tear down and delete database volumes
pnpm localenv:compose down --volumes
# tear down, delete database volumes and remove images
pnpm localenv:compose down --volumes --rmi all
```
### [Commands](#commands)
[Section titled “Commands”](#commands)
The following are the most commonly used commands:
| Description | Command |
| ----------------------------------------------------------------- | ------------------------------------------------ |
| Show all merged config (with TigerBeetle) | `pnpm localenv:compose config` |
| Start (with TigerBeetle) | `pnpm localenv:compose up` |
| Start (with TigerBeetle) detached | `pnpm localenv:compose up -d` |
| Down (with TigerBeetle) | `pnpm localenv:compose down` |
| Down and remove volumes (with TigerBeetle) | `pnpm localenv:compose down --volumes` |
| Down and remove volumes (with TigerBeetle) and images | `pnpm localenv:compose down --volumes --rmi all` |
| Show all merged config (with Postgres) | `pnpm localenv:compose:psql config` |
| Build all the containers (with TigerBeetle) | `pnpm localenv:compose build` |
| Start (with Postgres) | `pnpm localenv:compose:psql up` |
| Start (with Postgres) detached | `pnpm localenv:compose:psql up -d` |
| Down (with Postgres) | `pnpm localenv:compose:psql down` |
| Down (with Postgres) and remove volumes | `pnpm localenv:compose:psql down --volumes` |
| Build all the containers (with Postgres) | `pnpm localenv:compose:psql build` |
| Start with local admin auth enabled (this is disabled by default) | `pnpm localenv:compose:adminauth up` |
Enabling TigerBeetle
When enabled, TigerBeetle is only used by the primary instance (Cloud Nine Wallet). The secondary instance (Happy Life Bank) will always run Postgres as its accounting database.
### [Interacting with the local playground](#interacting-with-the-local-playground)
[Section titled “Interacting with the local playground”](#interacting-with-the-local-playground)
#### [Bruno & Open Payments APIs](#bruno--open-payments-apis)
[Section titled “Bruno & Open Payments APIs”](#bruno--open-payments-apis)
The Open Payments APIs can be interacted with using the [Bruno collection](https://github.com/interledger/rafiki/tree/main/bruno/collections/Rafiki) ([resource server endpoints](https://github.com/interledger/rafiki/tree/main/bruno/collections/Rafiki/Open%20Payments%20APIs) and [auth server endpoints](https://github.com/interledger/rafiki/tree/main/bruno/collections/Rafiki/Open%20Payments%20Auth%20APIs)). To interact with the Open Payments APIs using Bruno, you must:
1. Load the collection into Bruno by selecting **Open Collection**.
2. Navigate to `/rafiki/bruno/collections/Rafiki` on your machine and select **Open**.
3. Furthermore, you need to either load the [Local Environment](https://github.com/interledger/rafiki/tree/main/bruno/collections/Rafiki/environments/Local%20Playground.bru) or the [Remote Test Network Environment](https://github.com/interledger/rafiki/tree/main/bruno/collections/Rafiki/environments/Remote.bru).
Troubleshooting Bruno errors
If you receive the error below while running the Bruno collection, try switching from [Safe Mode to Developer Mode](https://docs.usebruno.com/get-started/javascript-sandbox)
``Error invoking remote method `send-http-request`: Error : Error: Cannot find module crypto``
The Examples folder in the Bruno collection includes an [Open Payments](https://github.com/interledger/rafiki/tree/main/bruno/collections/Rafiki/Examples/Open%20Payments) example that can be executed in the following sequence:
```
sequenceDiagram
participant B as Bruno client
participant SW as Sender's wallet
participant RW as Recipient's wallet
participant AS as Authorization server
participant IdP as Identity provider
B->>SW: 1. GET wallet address
SW-->>B: 200 OK (authServer URL)
B->>RW: 2. GET wallet address
RW-->>B: 200 OK (authServer URL)
Note over B,AS: Non-interactive grant for incoming payment resource
B->>AS: 3. POST grant request (incoming-payment)
AS-->>B: 200 OK (accessToken)
B->>RW: 4. POST /incoming-payments
RW-->>B: 201 Created (incomingPaymentId)
Note over B,AS: Non-interactive grant for quote resource
B->>AS: 5. POST grant request (quote)
AS-->>B: 200 OK (accessToken)
B->>SW: 6. POST /quotes
SW-->>B: 201 Created (quoteId)
Note over B,AS: Interactive grant required for outgoing payment resource
B->>AS: 7. POST grant request (outgoing-payment)
AS-->>B: 200 OK (redirect_uri)
rect rgb(240, 240, 240)
Note over B,AS: Interactive authorization
User interaction required to obtain consent
B->>IdP: Redirect for user consent
IdP-->>B: User consents
B->>AS: 8. POST /continue/{continueId}
end
AS-->>B: 200 OK (accessToken)
B->>SW: 9. POST /outgoing-payments
SW-->>B: 201 Created (outgoingPaymentId)
B->>SW: 10. GET /outgoing-payments/{outgoingPaymentId}
SW-->>B: 200 OK
```
View full diagramDownload diagram
The sequence of steps outlined below corresponds to the interactions depicted in the diagram above:
1. Requests the sender’s wallet address
2. Requests the receiver’s wallet address
3. Requests a grant to create an incoming payment on the receiver’s account
4. Creates an incoming payment on receiver’s account
5. Requests a grant to create and read a quote on the sender’s account
6. Creates a quote on the sender’s account
7. Requests a grant to create and read an outgoing payment on the sender’s account
Note
You have to go through an interaction flow by selecting the `redirect` link in the grant request’s response. More information about the interaction flow can be found [here](https://openpayments.dev/identity/grants/).
8. Continues the grant request
9. Creates an outgoing payment on the sender’s account
10. Fetches the outgoing payment on the sender’s account
#### [Admin APIs](#admin-apis)
[Section titled “Admin APIs”](#admin-apis)
In addition to using Rafiki Admin to interact with the Admin APIs, you can also use the Apollo Explorer on `localhost:3001/graphql` and `localhost:4001/graphql` for Cloud Nine Wallet and Happy Life Bank, respectively, and via the [Bruno collection](https://github.com/interledger/rafiki/tree/main/bruno/collections/Rafiki/Rafiki%20Admin%20APIs). The Bruno collection is configured to use the local environment’s default endpoints.
#### [SPSP](#spsp)
[Section titled “SPSP”](#spsp)
Every wallet address also serves as an SPSP endpoint. A `GET` request with an `Accept` header `application/spsp4+json` will return an SPSP response with STREAM connection details. The following example uses `http://localhost:3000/accounts/gfranklin` as the SPSP endpoint.
```sh
GET http://localhost:3000/accounts/gfranklin HTTP/1.1
Host:backend
Accept:application/spsp4+json
HTTP/1.1 200 OK
Connection:keep-alive
Content-Length:220
Content-Type:application/spsp4+json
Date:Thu, 23 Feb 2023 13:07:24 GMT
Keep-Alive:timeout=5
{
"destination_account": "test.rafiki.viXmy1OVHgvmQakNjX1C6kQM",
"shared_secret": "Rz_vudcg13EPs8ehL2drvZFJS1LJ4Y3EltOI60-lQ78"
}
```
### [Known issues](#known-issues)
[Section titled “Known issues”](#known-issues)
#### [TigerBeetle container exists with code 137](#tigerbeetle-container-exists-with-code-137)
[Section titled “TigerBeetle container exists with code 137”](#tigerbeetle-container-exists-with-code-137)
There is a known [issue](https://docs.tigerbeetle.com/operating/docker/#exited-with-code-137) when running TigerBeetle in Docker. The container exits without logs and simply shows error code 137. To fix this, increase the Docker memory limit. If you run the local Docker playground on a Windows machine via the Windows Subsystem for Linux (WSL), you can increase the memory limit by [configuring](https://learn.microsoft.com/en-us/windows/wsl/wsl-config#example-wslconfig-file) your `.wslconfig` file.
# Testnet
## [The Test Network](#the-test-network)
[Section titled “The Test Network”](#the-test-network)
The [local playground](/integration/playground/overview/) isn’t the only way to test and try out Rafiki. You can also try the Test Network, which is a Rafiki implementation bundled with a wallet and an e-commerce app available online. As it’s a test environment, you can experiment integrating with the Interledger network without using real money.
### [Applications](#applications)
[Section titled “Applications”](#applications)
The current applications include:
* [An Interledger test wallet](https://wallet.interledger-test.dev/)
* [An e-commerce app](https://boutique.interledger-test.dev/products)
## [Peering with the Test Network](#peering-with-the-test-network)
[Section titled “Peering with the Test Network”](#peering-with-the-test-network)
If you have installed the [Local Playground](/integration/playground/overview/) you can peer your local Rafiki instance with the remote Test Network. Refer to the [autopeering](/integration/playground/autopeering/) page for more details.
# Assets
An asset represents an item of value that can be transferred via the Interledger Protocol. Assets in Rafiki are added through the Backend Admin API or the [Rafiki Admin](/admin/admin-user-guide/#assets) app.
**Permissions**
Operators can create, view, edit, and delete both their own assets and those that belong to any tenant.
Tenants can create, view, edit, and delete their own assets only.
## [Add an asset](#add-an-asset)
[Section titled “Add an asset”](#add-an-asset)
Use the `createAsset` mutation to register a new asset with your Rafiki instance.
* Operation
```graphql
mutation CreateAsset($input: CreateAssetInput!) {
createAsset(input: $input) {
code
success
message
asset {
id
code
scale
tenantId
}
}
}
```
* Variables
```json
{
"input": {
"code": "USD",
"scale": 2
}
}
```
For more information about this mutation’s input object, see [`CreateAssetInput`](/apis/graphql/backend/#definition-CreateAssetInput).
Tenant ID and HMAC-signed request headers
Include a `tenant-id` header and a `signature` header (HMAC SHA-256) on each Backend Admin API request. Rafiki validates the signature and derives the tenant from these headers.
* Response
```json
{
"data": {
"createAsset": {
"code": "200",
"success": true,
"message": "Created Asset",
"asset": {
"id": "b3dffeda-1e0e-47d4-82a3-69b1a622eeb9",
"code": "USD",
"scale": 2,
"tenantId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
}
}
}
```
Tenant ID in the asset response
The `asset` object in the response will include the `tenantId` of the tenant to which the asset belongs. This `tenantId` is used to identify the tenant when processing requests related to the asset.
## [Update an asset](#update-an-asset)
[Section titled “Update an asset”](#update-an-asset)
Once an asset is created, you may need to adjust its operational threshold values. You can use the `updateAsset` mutation to modify the `liquidityThreshold`.
This threshold determines when Rafiki triggers low-liquidity notifications or webhooks, but doesn’t directly deposit or withdraw asset liquidity.
For information about adding or removing liquidity, see [Asset liquidity](/admin/liquidity/asset-liquidity).
* Operation
```graphql
mutation UpdateAsset($input: UpdateAssetInput!) {
updateAsset(input: $input) {
asset {
id
code
scale
withdrawalThreshold
liquidityThreshold
tenantId
}
}
}
```
* Variables
The input object for the update operation requires the asset `id`. Only the threshold fields are editable; all other fields are immutable.
```json
{
"input": {
"id": "b3dffeda-1e0e-47d4-82a3-69b1a622eeb9",
"withdrawalThreshold": 100,
"liquidityThreshold": 500
}
}
```
For more information about this mutation’s input object, see [`UpdateAssetInput`](/apis/graphql/backend/#definition-UpdateAssetInput).
Tenant ID and HMAC-signed request headers
Include a `tenant-id` header and a `signature` header (HMAC SHA-256) on each Backend Admin API request. Rafiki validates the signature and derives the tenant from these headers.
* Response
```json
{
"data": {
"updateAsset": {
"asset": {
"id": "b3dffeda-1e0e-47d4-82a3-69b1a622eeb9",
"code": "USD",
"scale": 2,
"withdrawalThreshold": 100,
"liquidityThreshold": 500,
"tenantId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
}
}
}
```
Tenant ID in the asset response
The `asset` object in the response will include the `tenantId` of the tenant to which the asset belongs. This `tenantId` is used to identify the tenant when processing requests related to the asset.
***
## [Delete an asset](#delete-an-asset)
[Section titled “Delete an asset”](#delete-an-asset)
Deleting an asset removes it from active use in a Rafiki instance.
You can only delete an asset if it’s not associated with any peers or wallet addresses. Rafiki prevents deleting any asset that’s still in use.
* Operation
```graphql
mutation DeleteAsset($input: DeleteAssetInput!) {
deleteAsset(input: $input) {
asset {
id
code
scale
tenantId
}
}
}
```
* Variables
```json
{
"input": {
"id": "b3dffeda-1e0e-47d4-82a3-69b1a622eeb9"
}
}
```
For more information about this mutation’s input object, see [`DeleteAssetInput`](/apis/graphql/backend/#definition-DeleteAssetInput).
Tenant ID and HMAC-signed request headers
Include a `tenant-id` header and a `signature` header (HMAC SHA-256) on each Backend Admin API request. Rafiki validates the signature and derives the tenant from these headers.
* Response
```json
{
"data": {
"deleteAsset": {
"asset": {
"id": "b3dffeda-1e0e-47d4-82a3-69b1a622eeb9",
"code": "USD",
"scale": 2,
"tenantId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
}
}
}
```
Tenant ID in the asset response
The `asset` object in the response will include the `tenantId` of the tenant to which the asset belongs. This `tenantId` is used to identify the tenant when processing requests related to the asset.
# Exchange rates
If you plan to support cross-currency transactions, you must specify where your Rafiki instance fetches exchange rates. Tenants may host their own rates endpoint, but if one isn’t configured, Rafiki falls back to the default `EXCHANGE_RATES_URL`.
A rate probe precedes every Interledger payment. The probe provides a quote that estimates the full cost of transferring value over the network. For a rate probe involving a cross-currency transaction to be successful, Rafiki needs to know the exchange rates for each currency that makes up the transaction.
Often, it’s the receiving ASE that provides the exchange rates for each ILP packet. For example, say you and your peer transact in USD. Your peer also supports MXN. If a USD payment from your side is addressed to a wallet address set up for MXN on your peer’s side, then your peer would provide the USD to MXN exchange rate.
## [Specify your exchange rates endpoint](#specify-your-exchange-rates-endpoint)
[Section titled “Specify your exchange rates endpoint”](#specify-your-exchange-rates-endpoint)
Operators configure a tenant-specific exchange rates endpoint using the Backend Admin API. If a tenant doesn’t have one configured, Rafiki uses the default `EXCHANGE_RATES_URL` environment variable. An [OpenAPI specification](https://github.com/interledger/rafiki/blob/main/packages/backend/src/openapi/specs/exchange-rates.yaml) for the endpoint is available.
Example
```bash
EXCHANGE_RATES_URL: http://cloud-nine-wallet/rates
```
The endpoint must accept GET requests and respond as follows.
Example API request
```bash
GET https://cloud-nine-wallet/rates
```
Example API response
```bash
{
"base": "USD",
"rates": {
"EUR": 0.813399,
}
}
```
### [Response objects](#response-objects)
[Section titled “Response objects”](#response-objects)
| Variable | Type | Description | Required |
| -------------------- | ------ | ------------------------------------------------------------------------------------------------------------------ | -------- |
| `base` | String | The asset code represented as an [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217) currency code, for example USD | Y |
| `rates` | Object | Object containing `` pairs, for example `{EUR: 0.8930}` | Y |
| `rates.` | Number | The exchange rate given `base` and `` | Y |
## [Specify rate caching duration (optional)](#specify-rate-caching-duration-optional)
[Section titled “Specify rate caching duration (optional)”](#specify-rate-caching-duration-optional)
Specify how long your Rafiki instance will cache exchange rates via the `backend` service’s `EXCHANGE_RATES_LIFETIME` variable or use the default setting of `15_000` ms (15 seconds).
Caching improves performance as Rafiki won’t need to request the rates from your endpoint for every payment.
## [Specify slippage (optional)](#specify-slippage-optional)
[Section titled “Specify slippage (optional)”](#specify-slippage-optional)
As exchange rates and fees charged by connectors fluctuate, there will likely be a variance between the estimated amount provided in the quote and the actual amount required when the payment is initiated. This difference is called slippage.
Set your allowed slippage rate to a value between 0 and 1 via the `backend` service’s `SLIPPAGE` variable or use the default setting of `0.01` (1%).
Example
```bash
SLIPPAGE: 0.05
```
### [Example](#example)
[Section titled “Example”](#example)
Let’s say your Rafiki instance is using the default slippage of `0.01` (1%). The rate probe that precedes a USD payment returns a quote of `$1.00`. One percent of one dollar equals one cent.
If the total of the payment, inclusive of currency exchange rates and network fees, amounts to `$1.01`, the payment will be successful. If the total is `$1.02` or more, the payment will fail.
Below is a minimalistic example of a successful (200) response.
```json
export function loader({ request }: LoaderFunctionArgs) {
const base = new URL(request.url).searchParams.get('base') || 'USD'
return json(
{
base,
rates: config.seed.rates[base] || {}
},
{ status: 200 }
)
}
```
## [Environment variables](#environment-variables)
[Section titled “Environment variables”](#environment-variables)
| Variable | Type | Description | Required |
| ------------------------- | --------- | ----------------------------------------------------------------------------------------------------------- | -------- |
| `EXCHANGE_RATES_URL` | `backend` | The default exchange rates endpoint. This URL is used if a tenant-specific rates endpoint isn’t configured. | Y |
| `EXCHANGE_RATES_LIFETIME` | `backend` | The amount of time Rafiki caches exchange rates, in ms | Y |
| `SLIPPAGE` | `backend` | The variance allowed between a quote and the actual amount required when a payment is initiated | Y |
# Viewing and revoking grants
Grants are the mechanism in Open Payments by which your account holders give permission to a client app to access their accounts and send payments on their behalf. Providing your account holders the ability to view and revoke grants isn’t required to implement and operate Rafiki. However, allowing them to do so is critical to providing an optimal user experience.
## [View grants](#view-grants)
[Section titled “View grants”](#view-grants)
Use the `Grants` GraphQL query to look up all grants associated with a wallet address.
* Operation
```graphql
query Grants(
$after: String
$before: String
$first: Int
$last: Int
$filter: GrantFilter
) {
grants(
after: $after
before: $before
first: $first
last: $last
filter: $filter
) {
edges {
cursor
node {
id
client
createdAt
state
access {
createdAt
id
identifier
limits {
interval
receiveAmount {
assetScale
value
assetCode
}
receiver
debitAmount {
assetCode
assetScale
value
}
}
actions
type
}
}
}
pageInfo {
endCursor
hasNextPage
hasPreviousPage
startCursor
}
}
}
```
* Variables
```json
{
"input": {
"after": null,
"before": null,
"first": null,
"last": null,
"filter": {
"state": {
"in": ["PROCESSING", "PENDING", "APPROVED", "FINALIZED"]
},
"identifier": {
"in": ["https://cloud-nine-wallet-backend/accounts/gfranklin"]
}
}
}
}
```
For more information about this query’s variables, see [`grants`](/apis/graphql/auth/#query-grants).
* Response
```json
{
"data": {
"grants": {
"edges": [
{
"cursor": "82637448-30d2-4242-9c85-464821dfbaf5",
"node": {
"id": "82637448-30d2-4242-9c85-464821dfbaf5",
"client": "https://happy-life-bank-backend/accounts/pfry",
"createdAt": "2025-03-27T13:48:23.615Z",
"state": "APPROVED",
"access": [
{
"createdAt": "2025-03-27T13:48:23.617Z",
"id": "05a5413b-7009-4ce1-949a-e0ff1b243268",
"identifier": "https://cloud-nine-wallet-backend/accounts/gfranklin",
"limits": {
"interval": null,
"receiveAmount": {
"assetScale": 2,
"value": "100",
"assetCode": "USD"
},
"receiver": null,
"debitAmount": {
"assetCode": "USD",
"assetScale": 2,
"value": "205"
}
},
"actions": [
"create",
"read",
"list"
],
"type": "outgoing-payment"
}
]
}
}
],
"pageInfo": {
"endCursor": "82637448-30d2-4242-9c85-464821dfbaf5",
"hasNextPage": false,
"hasPreviousPage": false,
"startCursor": "82637448-30d2-4242-9c85-464821dfbaf5"
}
}
}
}
```
## [Revoke a grant](#revoke-a-grant)
[Section titled “Revoke a grant”](#revoke-a-grant)
Use the `revokeGrant` GraphQL mutation to revoke a particular grant.
* Operation
```graphql
mutation revokeGrant($input: RevokeGrantInput!) {
revokeGrant(input: $input) {
id
}
}
```
* Variables
```json
{
"input": {
"grantId": "2117891e-4b89-42ae-984e-e0762d5888c1"
}
}
```
For more information about this mutation’s input object, see [`RevokeGrantInput`](/apis/graphql/auth/#definition-RevokeGrantInput).
* Response
```json
{
"data": {
"revokeGrant": {
"id": "2117891e-4b89-42ae-984e-e0762d5888c1"
}
}
}
```
# Identity provider (IdP)
An identity provider (IdP) is a system or service that stores and manages user identity information, authentication, and consent. Examples of IdPs include OpenID Connect and Okta.
Open Payments requires any authorization server that issues interactive grants be integrated with an IdP. Interactive grants are used to gather consent. More information about interactive grants is available [below](#interactive-grants).
Responsibilities of your IdP include:
* Providing an interface to gather end-user consent for a particular action
* Sending the interaction choice (approve or deny) to the authorization server
* Sending a request to the authorization server to finish the interaction
* Redirecting the user after the interaction is complete
Each tenant must use their own IdP.
Note
We provide Ory Kratos, a cloud-based user management system, for the identity and user management of your Rafiki Admin users. Kratos is for internal use only and **cannot** be used as your IdP for Open Payments.
## [Interactive grants](#interactive-grants)
[Section titled “Interactive grants”](#interactive-grants)
In Open Payments, grants indicate a resource owner, such as an account holder, has given a piece of software, such as a mobile app, permission (consent) to act on their behalf.
Rafiki’s implementation of an Open Payments authorization server requires that consent is collected via an interactive grant before an [outgoing payment](https://openpayments.dev/concepts/resources/#outgoing-payment) request is issued. A grant is interactive when explicit interaction by a resource owner (for example, the software’s end user) is required to approve or deny the grant. Tapping an *Approve* button to authorize a payment is an example of an explicit interaction.
Interactive grants can be optional for incoming payments and quotes; however, they’re enabled by default in Rafiki (the `LIST_ALL_ACCESS_INTERACTION` environment variable is `true`). When a grant request includes a `list-all` action for incoming payments and quotes, the request requires interaction. The `list-all` action is used when the client asks to list resources that it didn’t create.
If `LIST_ALL_ACCESS_INTERACTION` is `false`, you can still force interactive grants for quotes and/or incoming payments by setting the respective variables to `true`.
* `QUOTE_INTERACTION`
* `INCOMING_PAYMENT_INTERACTION`
See the [Open Payments documentation](https://openpayments.dev/identity/grants/) for more information on grant negotiation and authorization.
## [Authorization servers](#authorization-servers)
[Section titled “Authorization servers”](#authorization-servers)
Authorization servers grant permission to clients to access the Open Payments Resource APIs. This enables clients to create incoming payments, quotes, and outgoing payments against an account holder’s account.
Rafiki’s [auth service](/integration/deployment/services/auth-service) provides you with a reference implementation of an Open Payments authorization server. You can use the service as an alternative to developing your own in-house service.
Rafiki’s authorization server also extends an [API](#interaction-endpoints) that provides interaction endpoints for your IdP.
### [Environment variables](#environment-variables)
[Section titled “Environment variables”](#environment-variables)
The following variables configure the `auth` service. Operators set the `IDENTITY_SERVER_URL` and `IDENTITY_SERVER_SECRET` variables per tenant via the Backend Admin API. The `backend` service persists these values and keeps them in sync with the `auth` service’s tenant table and are used to secure communications between the `auth` service and the tenant’s IdP.
| Variable | Helm value name | Default | Description |
| ------------------------------ | ---------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `IDENTITY_SERVER_URL` | `auth.identityServer.domain` | N/A | The URL of your IdP’s server, used by the authorization server to tell an Open Payments client where to redirect the end-user to start interactions. |
| `IDENTITY_SERVER_SECRET` | `auth.identityServer.secret` | N/A | A shared secret between the authorization server and the IdP server; the authorization server uses the secret to secure its IdP-related endpoints. When the IdP server sends requests to the authorization server, the IdP server must provide the secret via an [`x-idp-secret`](#x-idp-secret-header) header. |
| `INCOMING_PAYMENT_INTERACTION` | `auth.interaction.incomingPayment` | `false` | Indicates whether incoming payments grant requests are interactive. |
| `INTERACTION_EXPIRY_SECONDS` | `auth.interactionExpirySeconds` | `600` | The time in seconds for which a user can interact with a grant request |
| `INTERACTION_PORT` | `auth.port.interaction` | `3009` | The port number for the [interaction endpoints](#interaction-endpoints) |
| `LIST_ALL_ACCESS_INTERACTION` | N/A | `true` | Specifies whether grant requests including a `list-all` action should require interaction. In these requests, the client asks to list resources that they themselves didn’t create. |
| `QUOTE_INTERACTION` | `auth.interaction.quote` | `false` | When `true`, quote grants are interactive. |
## [Interaction endpoints](#interaction-endpoints)
[Section titled “Interaction endpoints”](#interaction-endpoints)
The authorization server provided by Rafiki’s `auth` service extends an API for an IdP server to use after a pending grant request is created.
Each interaction with an endpoint is identified by an `id` and a `nonce`. Both are provided as query parameters when the authorization server redirects to the IdP server.
The endpoints are tied to the auth server URL. For example, if your auth server URL is `https://auth.wallet.example.com`, then calling the `/interact/{id}/{nonce}` endpoint to start a user interaction session would look as follows:
```plaintext
https://auth.wallet.example.com/interact/{id}/{nonce}
```
### [Interaction endpoints](#interaction-endpoints-1)
[Section titled “Interaction endpoints”](#interaction-endpoints-1)
The endpoints are called in the sequence listed below.
| Method | Endpoint | Purpose | Called by | Publicly exposed |
| ------ | ------------------------------- | ----------------------------------------------------------------- | -------------------- | ---------------- |
| GET | `/interact/{id}/{nonce}` | [Start user interaction session](#start-user-interaction-session) | Open Payments client | Yes |
| GET | `/grant/{id}/{nonce}` | [Look up grant information](#look-up-grant-information) | Identity provider | No |
| POST | `/grant/{id}/{nonce}/{choice}` | [Accept or reject grant](#accept-or-reject-grant) | Identity provider | No |
| GET | `/interact/{id}/{nonce}/finish` | [Finish user interaction](#finish-interaction) | Identity provider | Yes |
| POST | `/interact/{id}/{nonce}` | [Continue grant](#continue-grant) | Open Payments client | Yes |
We also provide an [OpenAPI specification](https://github.com/interledger/rafiki/blob/main/packages/auth/src/openapi/specs/id-provider.yaml) that describes the endpoints. Note that the *Continue grant* endpoint isn’t included in the spec because it’s part of the [Open Payments Auth Server API](https://openpayments.dev/apis/auth-server/operations/post-continue/).
#### [Start user interaction session](#start-user-interaction-session)
[Section titled “Start user interaction session”](#start-user-interaction-session)
Called by the client to establish an interactive session with the authorization server. The authorization server automatically redirects the request, via the URL defined in the `IDENTITY_SERVER_URL` variable, to your IdP consent screen.
#### [Look up grant information](#look-up-grant-information)
[Section titled “Look up grant information”](#look-up-grant-information)
Called by the IdP server to retrieve a list of access rights, requested by the client, from the authorization server. The request is secured with an [`x-idp-secret`](#x-idp-secret-header) header. The access rights are presented to the client’s end-user on the consent screen. The authorization server’s response is served on your configured `INTERACTION_PORT`.
#### [Accept or reject grant](#accept-or-reject-grant)
[Section titled “Accept or reject grant”](#accept-or-reject-grant)
The IdP server communicates the choice made by the end-user on the consent screen (accept/reject) to the authorization server. The request is secured with an [`x-idp-secret`](#x-idp-secret-header) header. The authorization server responds to the IdP server, acknowledging that it received the request.
#### [Finish interaction](#finish-interaction)
[Section titled “Finish interaction”](#finish-interaction)
Called by the IdP server to end the interaction. If a `finish` URI was provided in the original grant initialization request, the authorization server redirects the user to that URI.
The `result` query parameter in the response indicates the success or failure of the grant authorization. The following are examples of the possible response types.
| Response | Description | Example |
| -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| Rejected | The end-user rejected the interaction | `?result=grant_rejected` |
| Invalid | The grant wasn’t in a state where it could be accepted or rejected (for example, the grant was already approved) | `?result=grant_invalid` |
| Success | The grant was successful with the following returned in the response: - A hash representing the SHA-256 hash of values provided by the client in the [grant initialization request](https://openpayments.dev/apis/auth-server/operations/post-request/) (`interact.finish.nonce`), and the values in the response returned from the authorization server (`interact.finish`).
- The `interact_ref` that identifies the interaction on the authorization server alongside the hash
- The URI of the grant initialization request (for example, `https://www.auth-server.com`) | `hash=p28jsq0Y2KK3WS__a42tavNC64ldGTBroywsWxT4md_jZQ1R\HZT8BOWYHcLmObM7XHPAdJzTZMtKBsaraJ64A &interact_ref=4IFWWIKYBC2PQ6U56NL1` |
When successful, the SHA-256 hash of the interaction is sent in the response to the client. An `interact_ref` that identifies the interaction on the authorization server and the URI of the grant initialization request are also included.
#### [Continue grant](#continue-grant)
[Section titled “Continue grant”](#continue-grant)
The client requests a grant from the authorization server for an accepted interaction. The authorization server responds with an [access token](https://openpayments.dev/apis/auth-server/operations/post-continue/).
## [X-idp-secret header](#x-idp-secret-header)
[Section titled “X-idp-secret header”](#x-idp-secret-header)
The `x-idp-secret` header is specific to Rafiki’s authorization server and is used for requests to the following endpoints:
* `GET /grant/:id/:nonce`
* `POST /grant/:id/:nonce/accept`
* `POST /grant/:id/:nonce/reject`
The header’s purpose is to secure communications between the IdP and the authorization server. Its value should be the `IDENTITY_SERVER_SECRET` configured for the specific tenant. When the IdP server sends requests to the authorization server, the IdP must provide the secret via this header.
Note
If you’re running your own authorization server rather than using the server provided by Rafiki, you can add security in any way you see fit. You aren’t required to use the `x-idp-secret` header.
To set up the header, set the `IDENTITY_SERVER_SECRET` variable to a value that’s also used to configure your IdP server’s requests to the authorization server.
# Wallet address keys
Creating a public-private key pair for each wallet address isn’t required when integrating with Rafiki.
You only need to create key pairs for wallet addresses if you want to allow your account holders to use/be Open Payments clients under their wallet addresses. For more information, review the Open Payments documentation about [clients](https://openpayments.dev/resources/glossary/#client) and [client keys](https://openpayments.dev/identity/client-keys/).
## [Create a wallet address key pair](#create-a-wallet-address-key-pair)
[Section titled “Create a wallet address key pair”](#create-a-wallet-address-key-pair)
Use the `createWalletAddressKey` GraphQL mutation to create a key pair and associate it with a wallet address.
* Operation
```graphql
mutation CreateWalletAddressKey($input: CreateWalletAddressKeyInput!) {
createWalletAddressKey(input: $input) {
code
message
success
walletAddressKey {
id
walletAddressId
revoked
jwk {
alg
crv
kid
kty
x
}
createdAt
}
}
}
```
* Variables
```json
{
"input": {
"jwk": {
"kid": "keyid-97a3a431-8ee1-48fc-ac85-70e2f5eba8e5",
"x": "ubqoInifJ5sssIPPnQR1gVPfmoZnJtPhTkyMXNoJF_8",
"alg": "EdDSA",
"kty": "OKP",
"crv": "Ed25519"
},
"walletAddressId": "695e7546-1803-4b45-96b6-6a53f4082018"
}
}
```
The request is a standard request to create a JSON Web Key (JWK), which is a JSON data structure that represents a cryptographic key. [Section 4](https://datatracker.ietf.org/doc/html/rfc7517#section-4) of the JWK specification describes the format and associated parameters `kty`, `alg`, and `kid`. [Section 6](https://datatracker.ietf.org/doc/html/rfc7518#section-6) of the JSON Web Algorithms (JWA) specification describes the cryptographic algorithm for the keys and associated parameters `kty`, `crv`, and `x`.
Open Payments [requires](https://openpayments.dev/apis/wallet-address-server/operations/get-wallet-address-keys/) the following values.
| Parameter | Required value | Description |
| --------- | -------------- | ----------------------------------------------------------------------------- |
| `alg` | `EdDSA` | The algorithm used to generate the key pair |
| `kty` | `OKP` | The key type identifying the cryptographic algorithm family used with the key |
| `crv` | `Ed25519` | The cryptographic curve used with the key |
Additionally, the request must contain the `walletAddressId` of the wallet address that the key pair will be associated with.
* Response
```json
{
"data": {
"createWalletAddressKey": {
"code": "200",
"message": "Added Key To Wallet Address",
"success": true,
"walletAddressKey": {
"id": "f2953571-f10c-44eb-ab41-4450a7ad6771",
"walletAddressId": "695e7546-1803-4b45-96b6-6a53f4082018",
"revoked": false,
"jwk": {
"alg": "EdDSA",
"crv": "Ed25519",
"kid": "keyid-97a3a431-8ee1-48fc-ac85-70e2f5eba8e5",
"kty": "OKP",
"x": "ubqoInifJ5sssIPPnQR1gVPfmoZnJtPhTkyMXNoJF_8"
},
"createdAt": "2023-03-03T09:26:41.424Z"
}
}
}
}
```
## [Revoke a wallet address key](#revoke-a-wallet-address-key)
[Section titled “Revoke a wallet address key”](#revoke-a-wallet-address-key)
Use the `revokeWalletAddressKey` GraphQL mutation to revoke a public key associated with a wallet address. Open Payments requests using this key for request signatures will be denied going forward.
* Operation
```graphql
mutation RevokeWalletAddressKey($input: RevokeWalletAddressKeyInput!) {
revokeWalletAddressKey(input: $input) {
walletAddressKey {
id
revoked
walletAddressId
createdAt
}
}
}
```
* Variables
```json
{
"input": {
"id": "e7532552-cff9-4ffe-883e-56613d3ae611"
}
}
```
* Response
```json
{
"data": {
"revokeWalletAddressKey": {
"walletAddressKey": {
"id": "f2953571-f10c-44eb-ab41-4450a7ad6771",
"revoked": true,
"walletAddressId": "695e7546-1803-4b45-96b6-6a53f4082018",
"createdAt": "2023-03-03T09:26:41.424Z"
}
}
}
}
```
# Integration checklist
## [Required](#required)
[Section titled “Required”](#required)
Before deploying Rafiki to a production environment and joining the Interledger network, ensure you:
| | |
| - | ----------------------------------------------------------------------------------------------------------- |
| ✅ | Are a licensed financial account servicing entity (ASE) in the jurisdictions you operate in |
| ✅ | Generate a UUID v4 for your operator tenant ID and a strong, random admin API secret |
| ✅ | You are HMAC-signing Backend Admin API requests (HMAC SHA-256) and include tenant ID and signature headers |
| ✅ | Establish a secure, out-of-band process to deliver tenant IDs and API secrets to each tenant after creation |
| ✅ | Add at least one asset, either through the Backend Admin API or the Rafiki Admin app |
| ✅ | Implement a strategy for creating wallet addresses for your account holders |
| ✅ | Set up your webhook endpoint and understand how to handle each webhook event |
| ✅ | Secure your admin services from external access |
## [Optional](#optional)
[Section titled “Optional”](#optional)
| | |
| - | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ✅ | **Integrate with an identity provider (IdP)**If you plan to use the authorization server provided through Rafiki’s Auth service, and/or support Open Payments outgoing payments, you **must** [integrate with an IdP](/integration/requirements/open-payments/idp). An IdP is a system or service that stores and manages user identity information, authentication, and consent for an ASE’s users. |
| ✅ | **Add a peer**A peer is another ASE that you connect with via Interledger who is likely running their own Rafiki instance. If you are using Rafiki solely for transfers between accounts on your own ledger, peers aren’t required. Otherwise, you must [add at least one peer](/integration/requirements/peers) to enable Interledger payments on your accounts. |
| ✅ | **Set up your exchange rates endpoint**If you plan to support cross-currency transactions, you must [specify the endpoint](/integration/requirements/exchange-rates) from where your Rafiki instance will fetch current exchange rates. Exchange rates are calculated as part of a payment’s quote, which estimates the full cost of transferring value over the network. |
| ✅ | **Define your sending fee**You can [charge a sending fee](/integration/requirements/sending-fees), on top of any estimated network fees, for facilitating transfers. Each asset you support can have a different fee structure. |
# Peers
To join the Interledger network and be able to send and receive payments, you must add one or more peers to your Rafiki instance. Peering establishes the connections needed for your Rafiki instance to interact with another account servicing entity (ASE). The purpose of this guide is to help you set up and manage peers.
While this guide focuses on the conceptual and technical steps of adding and managing peers via the Backend Admin API, the Rafiki Admin app offers the same capabilities in a user-friendly interface.
Refer to the [Rafiki Admin user guide](/admin/admin-user-guide#peers) for detailed instructions and examples of creating and managing peers through the app.
Tip
Whether you are using the Backend Admin API or the Rafiki Admin app, the underlying configurations and requirements remain the same. Choose the interface that best suits your individual workflow.
**Permissions**
Operators can create, view, edit, and delete both their own peers and those that belong to any tenant.
Tenants can view, edit, and delete only their own peers. They can’t create peers.
## [Perform prerequisites](#perform-prerequisites)
[Section titled “Perform prerequisites”](#perform-prerequisites)
Note
Peering isn’t required unless you want to participate in transactions with another ASE on the Interledger network. For foundational peering concepts, refer to the Peers section of [Interledger Concepts](/overview/concepts/interledger/#peers).
Before adding a peer, you and the account servicing entity you intend to peer with must both:
### [Run an Interledger connector](#run-an-interledger-connector)
[Section titled “Run an Interledger connector”](#run-an-interledger-connector)
While you and your peer can run any implementation of an [Interledger connector](/integration/deployment/services/backend-service#interledger-connector) such as the [TypeScript implementation](https://github.com/interledger/interledgerjs), it’s recommended to use Rafiki.
### [Agree on an asset](#agree-on-an-asset)
[Section titled “Agree on an asset”](#agree-on-an-asset)
Both you and your peer must agree on an [asset](/overview/concepts/accounting#assets) for the peering relationship. You can set up multiple peering relationships with the same peer based on different assets. At least one asset shared by you and your peer must be added to your Rafiki instance before setting up the peering relationship. For more information, refer to [Assets](/integration/requirements/assets/).
### [Exchange static Interledger (ILP) addresses](#exchange-static-interledger-ilp-addresses)
[Section titled “Exchange static Interledger (ILP) addresses”](#exchange-static-interledger-ilp-addresses)
Your [ILP address](https://interledger.org/developers/rfcs/ilp-addresses/) is self-assigned during Rafiki setup and stored locally as the `ILP_ADDRESS` environment variable for the `backend` service.
### [Communicate a connection endpoint](#communicate-a-connection-endpoint)
[Section titled “Communicate a connection endpoint”](#communicate-a-connection-endpoint)
The connection endpoint will be a URL that the other peer will send packets to.
### [Exchange auth tokens for the connection endpoint](#exchange-auth-tokens-for-the-connection-endpoint)
[Section titled “Exchange auth tokens for the connection endpoint”](#exchange-auth-tokens-for-the-connection-endpoint)
Incoming `authtokens` allow you to authenticate that packets sent from your peer originated from your peer’s Interledger connector and weren’t tampered en route. The outgoing `authtoken` allows your peer to authenticate that received packets originated from your Interledger connector and weren’t tampered with en route. The use of auth tokens isn’t required when [autopeering with the Interledger test network](/integration/playground/autopeering).
### [Agree on a settlement mechanism](#agree-on-a-settlement-mechanism)
[Section titled “Agree on a settlement mechanism”](#agree-on-a-settlement-mechanism)
The settlement mechanism you both agree to use is what facilitates the transfer of actual funds between you and your peer. Neither Interledger nor Rafiki provide a settlement mechanism.
## [Perform optional prerequisites](#perform-optional-prerequisites)
[Section titled “Perform optional prerequisites”](#perform-optional-prerequisites)
### [Deposit an initial liquidity for your peer](#deposit-an-initial-liquidity-for-your-peer)
[Section titled “Deposit an initial liquidity for your peer”](#deposit-an-initial-liquidity-for-your-peer)
While you can deposit an `initialLiquidity` for your peer, you can also deposit liquidity later using the `depositPeerLiquidity` mutation.
### [Define a maxPacketAmount value](#define-a-maxpacketamount-value)
[Section titled “Define a maxPacketAmount value”](#define-a-maxpacketamount-value)
The `maxPacketAmount` specifies the maximum packet size you are willing to accept from the peer. Your peer’s `maxPacketAmount` value doesn’t need to match, as this value is independently set by each ASE. If omitted, payments won’t be broken into smaller packets.
## [Set up peering in Rafiki](#set-up-peering-in-rafiki)
[Section titled “Set up peering in Rafiki”](#set-up-peering-in-rafiki)
The basic workflow of setting up a peering relationship starts with adding the agreed upon asset and then adding a peer.
### [Add an asset](#add-an-asset)
[Section titled “Add an asset”](#add-an-asset)
As mentioned in the prerequisites, you must add an asset to your Rafiki instance before creating a peering relationship. To learn how to add an asset, refer to [Assets](/integration/requirements/assets/).
### [Add a peer](#add-a-peer)
[Section titled “Add a peer”](#add-a-peer)
* Operation
```graphql
mutation CreatePeer($input: CreatePeerInput!) {
createPeer(input: $input) {
code
success
message
peer {
id
asset {
code
scale
}
staticIlpAddress
name
tenantId
}
}
}
```
* Variables
```json
{
"input": {
"staticIlpAddress": "g.othergreatwallet",
"name": "The Other Great Wallet"
"http": {
"incoming": {"authTokens": ["mytoken"]},
"outgoing": {"endpoint": "ilp.othergreatwallet.com", "authToken": "theirtoken"}
},
"assetId": "INSERT_ASSET_ID",
"initialLiquidity":
}
}
```
For more information about this mutation’s input object, see [`CreatePeerInput`](/apis/graphql/backend/#definition-CreatePeerInput).
Tenant ID and HMAC-signed request headers
Include a `tenant-id` header and a `signature` header (HMAC SHA-256) on each Backend Admin API request. Rafiki validates the signature and derives the tenant from these headers.
* Response
```json
{
"data": {
"createPeer": {
"code": "200",
"success": true,
"message": "Created ILP Peer",
"peer": {
"id": "480ef339-7842-4501-a905-923fc1339cef",
"asset": {
"code": "USD",
"scale": 2
},
"staticIlpAddress": "g.othergreatwallet",
"name": "The Other Great Wallet",
"tenantId": "123e4567-e89b-12d3-a456-426614174000"
}
}
}
}
```
Tenant ID in the peer response
The `peer` object in the response will include the `tenantId` of the tenant to which the peer belongs. This `tenantId` is used to identify the tenant when processing requests related to the peer.
## [Manage peers](#manage-peers)
[Section titled “Manage peers”](#manage-peers)
Once a peer is added to your Rafiki instance, there is minimal ongoing management required. Most peer interactions focus on monitoring liquidity and ensuring smooth payment flows. In rare cases, you may need to update a peer’s configuration due to changes in their technical details or remove a peer created in error, as long as no payments have been exchanged. These actions help ensure your Rafiki instance stays up to date with operational changes.
### [Edit a peer](#edit-a-peer)
[Section titled “Edit a peer”](#edit-a-peer)
Occasionally, you may need to adjust peering configurations or address any changes communicated by the peer. Some examples include updating new endpoints or tokens, technical settings like the maximum packet amount, or peer liquidity thresholds.
In this example we will update the peer we just created. Rather than change any of the peering details, we can add some optional details that we didn’t include when we created the peer. We will define the `maxPacketAmount` and the `liquidityThreshold`.
* Operation
```graphql
mutation UpdatePeer($input: UpdatePeerInput!) {
updatePeer(input: $input) {
peer {
id
name
http {
outgoing {
authToken
endpoint
}
}
maxPacketAmount
liquidityThreshold
tenantId
}
}
}
```
* Variables
The input object for the update operation only requires that the `id` is present. All other variables are optional. For this example we will include the required `id` variable and the optional variables of the fields we wish to update. In this case, `maxPacketAmount` and `liquidityThreshold`..
```json
{
"input": {
"id": "480ef339-7842-4501-a905-923fc1339cef",
"maxPacketAmount": 1000,
"liquidityThreshold": 100
}
}
```
For more information about this mutation’s input object, see [`UpdatePeerInput`](/apis/graphql/backend/#definition-UpdatePeerInput).
Tenant ID and HMAC-signed request headers
Include a `tenant-id` header and a `signature` header (HMAC SHA-256) on each Backend Admin API request. Rafiki validates the signature and derives the tenant from these headers.
* Response
```json
{
"data": {
"updatePeer": {
"code": "200",
"success": true,
"message": "Updated ILP Peer",
"peer": {
"id": "480ef339-7842-4501-a905-923fc1339cef",
"name": "The Other Great Wallet",
"http": {
"outgoing": {
"authToken": "test",
"endpoint": "http://peering-test:3002"
}
},
"maxPacketAmount": 1000,
"liquidityThreshold": 100,
"tenantId": "123e4567-e89b-12d3-a456-426614174000"
}
}
}
}
```
Tenant ID in the peer response
The `peer` object in the response will include the `tenantId` of the tenant to which the peer belongs. This `tenantId` is used to identify the tenant when processing requests related to the peer.
### [Delete a peer](#delete-a-peer)
[Section titled “Delete a peer”](#delete-a-peer)
Deleting a peer is an action that removes a peer from your Rafiki instance. There are specific rules and considerations to keep in mind before starting this irreversible operation.
You can only delete a peer if no payments were sent to or received from that peer. This ensures that historical payment records are preserved. If you attempt to delete a peer with payment history, the backend throws an error, preventing the deletion.
Deleting a peer is useful in situations where there were configuration errors when the peer was first created like an incorrect auth token or ILP address.
Danger
Deleting a peer is permanent and can’t be reversed. If you delete a peer in error, you must create another new peer.
* Operation
```graphql
mutation DeletePeer($input: DeletePeerInput!) {
deletePeer(input: $input) {
success
}
}
```
* Variables
```json
{
"input": {
"id": "480ef339-7842-4501-a905-923fc1339cef"
}
}
```
For more information about this mutation’s input object, see [`DeletePeerInput`](/apis/graphql/backend/#definition-DeletePeerInput).
* Response
```json
{
"data": {
"deletePeer": {
"success": true
}
}
}
```
# Sending fees
You have the option to charge sending fees, on top of any estimated network fees, for facilitating transfers. Each asset you support can have a different fee structure and you can specify both fixed and variable fees per asset. The fee amount is added on top of the quote that’s generated after the ILP rate probe completes. You can define sending fees through the Backend Admin API or the [Rafiki Admin](/admin/admin-user-guide/#edit-asset) app.
## [Set sending fees](#set-sending-fees)
[Section titled “Set sending fees”](#set-sending-fees)
* Operation
```graphql
mutation SetFee($input: SetFeeInput!) {
setFee(input: $input) {
code
success
message
fee {
id
assetId
type
fixed
basisPoints
createdAt
}
}
}
```
* Variables
For this example, let’s assume your asset scale is 2. You’ll charge a fixed fee of 100 ($1.00) and a variable fee of 100 (1%).
```json
{
"input": {
"assetId": "14863f6f-4bda-42ef-8715-bf4762898af8",
"type": "SENDING",
"fee": {
"fixed": 100,
"basisPoints": 100
}
}
}
```
For more information about this mutation’s input object, see [`SetFeeInput`](/apis/graphql/backend/#definition-SetFeeInput).
* Response
```json
{
"data": {
"setFee": {
"code": "200",
"success": true,
"message": "Fee set",
"fee": {
"id": "140fd9c0-8f14-4850-9724-102f04d97e69",
"assetId": "14863f6f-4bda-42ef-8715-bf4762898af8",
"type": "SENDING",
"fixed": "100",
"basisPoints": 100,
"createdAt": "2023-09-13T14:59:53.435Z"
}
}
}
}
```
# Tenants
In Rafiki, a tenant represents an isolated environment for an account servicing entity (ASE). Each tenant has its own set of resources, such as assets, peers, and wallet addresses, and its own configuration settings. This allows multiple ASEs to share a single Rafiki instance while maintaining data isolation and security. The purpose of this guide is to help you set up and manage tenants.
While this guide focuses on operators managing tenants from the Backend Admin API, the Rafiki Admin app offers the same capabilities in a user-friendly interface.
Refer to the [Rafiki Admin user guide](/admin/admin-user-guide/#tenants) for detailed instructions and examples of creating and managing tenants through the app.
## [Tenant properties](#tenant-properties)
[Section titled “Tenant properties”](#tenant-properties)
Each tenant on a given Rafiki instance has the following properties:
| Property | Description |
| --------------- | ------------------------------------------------------------------------------------------------------------------- |
| `id` | Unique identifier for the tenant used in API requests and webhook events. |
| `email` | The tenant’s email address. |
| `apiSecret` | Secret used to HMAC-sign Backend Admin API requests (HMAC SHA-256) for this tenant. |
| `idpConsentUrl` | The tenant’s identity provider (IdP) consent URL used to redirect end-users for interactive grants (Open Payments). |
| `idpSecret` | Secret used to authenticate requests from the tenant’s IdP to Rafiki. |
| `publicName` | Public display name for the tenant (shown in the Rafiki Admin app). |
| `settings` | Key-value pairs for initial tenant settings. See the table below. |
## [Tenant settings](#tenant-settings)
[Section titled “Tenant settings”](#tenant-settings)
Tenant settings allow operators to customize tenant behavior. These settings are stored as key-value pairs and can be managed via the Backend Admin API or the Rafiki Admin app.
| Setting | Description |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `EXCHANGE_RATES_URL` | The URL of the tenant’s exchange rates service. This setting is used to configure the source of exchange rate data for the tenant. |
| `WEBHOOK_URL` | The URL of the tenant’s webhook endpoint. This setting is used to configure the endpoint that will receive webhook events for the tenant. |
| `WEBHOOK_TIMEOUT` | The timeout for the tenant’s webhook requests (in milliseconds). This setting is used to configure the maximum time to wait for a response from the webhook endpoint. |
| `WEBHOOK_MAX_RETRY` | The maximum number of retries for the tenant’s webhook event when a non-200 status is returned or if the request timed out. |
| `WALLET_ADDRESS_URL` | Base URL of wallet addresses created for the tenant. This setting **cannot** be updated once set. |
| `ILP_ADDRESS` | Base Interledger Protocol (ILP) address for the tenant. |
Note
Only operators can create, edit, and delete tenants.
## [Create a tenant](#create-a-tenant)
[Section titled “Create a tenant”](#create-a-tenant)
Use the `createTenant` mutation to register a new tenant in your Rafiki instance.
This operation is restricted to operators. When a new tenant is created, Rafiki automatically assigns a default ILP address, applies standard configuration settings, and registers the tenant with the `auth` service. Operators can also provide initial custom settings during creation.
After you create a tenant, securely communicate the tenant `id` and `apiSecret` to the tenant out-of-band.
* Operation
```graphql
mutation CreateTenant($input: CreateTenantInput!) {
createTenant(input: $input) {
tenant {
id
publicName
email
apiSecret
idpConsentUrl
idpSecret
}
}
}
```
* Variables
```json
{
"input": {
"publicName": "Tenant Name",
"email": "tenant@example.com",
"apiSecret": "your-secret-api-key",
"idpConsentUrl": "https://example.com/consent",
"idpSecret": "your-idp-secret"
}
}
```
For more information about this mutation’s input object, see [`CreateTenantInput`](/apis/graphql/backend/#definition-CreateTenantInput).
* Response
```json
{
"data": {
"createTenant": {
"tenant": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"publicName": "Tenant Name",
"email": "tenant@example.com",
"apiSecret": "your-secret-api-key",
"idpConsentUrl": "https://example.com/consent",
"idpSecret": "your-idp-secret"
}
}
}
}
```
## [Update a tenant](#update-a-tenant)
[Section titled “Update a tenant”](#update-a-tenant)
Use the `updateTenant` mutation to modify an existing tenant’s configuration.
Tenants can update their own profile details such as the public name, contact email, or identity provider URLs. Tenants can also rotate their own `apiSecret` using this mutation. Operators can update any tenant’s information, but can’t modify a tenant’s `apiSecret` on their behalf.
* Operation
```graphql
mutation UpdateTenant($input: UpdateTenantInput!) {
updateTenant(input: $input) {
tenant {
id
email
apiSecret
idpConsentUrl
idpSecret
publicName
}
}
}
```
* Variables
```json
{
"input": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"publicName": "New Tenant Name",
"email": "new-tenant@example.com",
"idpConsentUrl": "https://example.com/new-consent",
"idpSecret": "new-idp-secret"
}
}
```
For more information about this mutation’s input object, see [`UpdateTenantInput`](/apis/graphql/backend/#definition-UpdateTenantInput).
* Response
```json
{
"data": {
"updateTenant": {
"tenant": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"publicName": "New Tenant Name",
"email": "new-tenant@example.com",
"apiSecret": "your-secret-api-key",
"idpConsentUrl": "https://example.com/new-consent",
"idpSecret": "new-idp-secret"
}
}
}
}
```
## [Delete a tenant](#delete-a-tenant)
[Section titled “Delete a tenant”](#delete-a-tenant)
Use the `deleteTenant` mutation to remove a tenant from Rafiki. Only operators can perform this action.
Deleting a tenant marks the tenant as deleted and removes all associated tenant settings and authentication entries. After deletion, the tenant can no longer be used to create or manage resources in Rafiki.
* Operation
```graphql
mutation DeleteTenant($id: String!) {
deleteTenant(id: $id) {
success
}
}
```
* Variables
```json
{
"id": "123e4567-e89b-12d3-a456-426614174000"
}
```
* Response
```json
{
"data": {
"deleteTenant": {
"success": true
}
}
}
```
# Wallet addresses
Each payment account belonging to your users (for example, your customers) must have at least one associated wallet address for the account to be able to send and receive payments over Interledger and Open Payments. A wallet address serves as a publicly shareable standardized ID for a payment account. Each wallet address belongs to a specific tenant.
**Permissions**
* Operators can create wallet addresses for any tenant
* Tenants can only create wallet addresses for themselves
Wallet address requirements
* Your Rafiki instance must be set up with at least one asset before wallet addresses can be created as each wallet address must have an asset assigned to it.
* Wallet address URLs are treated as case-insensitive, meaning that both lowercase and uppercase variations of the same address will be recognized as identical.
* Operators must configure a wallet address prefix for each tenant. When creating wallet addresses, tenants are restricted to using this prefix.
Once the wallet address base (`WALLET_ADDRESS_URL`) is set for a tenant, it can’t be changed.
## [Create wallet addresses](#create-wallet-addresses)
[Section titled “Create wallet addresses”](#create-wallet-addresses)
There are a few ways you can create wallet addresses.
* [Through a script](#create-wallet-addresses-through-a-script)
* [In response to the `wallet_address.not_found` webhook event](#create-wallet-addresses-in-response-to-a-webhook-event)
* [In the Rafiki Admin app](/admin/admin-user-guide#create-wallet-address)
### [Create wallet addresses through a script](#create-wallet-addresses-through-a-script)
[Section titled “Create wallet addresses through a script”](#create-wallet-addresses-through-a-script)
Writing your own script that loops through your list of accounts is one way to batch-create wallet addresses for your existing account holders.
Ensure your script calls the `createWalletAddress` GraphQL mutation.
* Operation
```graphql
mutation CreateWalletAddress($input: CreateWalletAddressInput!) {
createWalletAddress(input: $input) {
code
success
message
walletAddress {
id
createdAt
publicName
url
asset {
code
id
scale
}
tenantId
}
}
}
```
We strongly recommend you store at least the `walletAddress.id` in your internal database to be able to reference the account and wallet address later.
* Variables
```json
{
"input": {
"assetId": "0ddc0b7d-1822-4213-948e-915dda58850b",
"publicName": "Sarah Marshall",
"url": "https://example.wallet.com/sarah",
"additionalProperties": [
{
"key": "iban",
"value": "NL93 8601 1117 947",
"visibleInOpenPayments": false
},
{
"key": "nickname",
"value": "S Mar",
"visibleInOpenPayments": true
}
]
}
}
```
For more information about this mutation’s input object, see [`CreateWalletAddressInput`](/apis/graphql/backend/#definition-CreateWalletAddressInput).
Tenant ID and HMAC-signed request headers
Include a `tenant-id` header and a `signature` header (HMAC SHA-256) on each Backend Admin API request. Rafiki validates the signature and derives the tenant from these headers.
* Response
```json
{
"data": {
"createWalletAddress": {
"code": "200",
"success": true,
"message": "Created wallet address",
"walletAddress": {
"id": "695e7546-1803-4b45-96b6-6a53f4082018",
"createdAt": "2023-03-03T09:07:01.107Z",
"publicName": "Sarah Marshall",
"url": "https://example.wallet.com/sarah",
"asset": {
"id": "0ddc0b7d-1822-4213-948e-915dda58850b",
"code": "USD",
"scale": 2
},
"tenantId": "123e4567-e89b-12d3-a456-426614174000"
}
}
}
}
```
Tenant ID in the wallet address response
The `walletAddress` object in the response will include the `tenantId` of the tenant to which the wallet address belongs. This `tenantId` is used to identify the tenant when processing Open Payments requests.
### [Create wallet addresses in response to a webhook event](#create-wallet-addresses-in-response-to-a-webhook-event)
[Section titled “Create wallet addresses in response to a webhook event”](#create-wallet-addresses-in-response-to-a-webhook-event)
The [`wallet_address.not_found`](/integration/requirements/webhook-events#wallet-address-not-found) event fires when a wallet address is requested through the Open Payments Get Wallet Address API, but Rafiki can’t find the address.
When you receive the event, look up the associated account in your system, then call the `createWalletAddress` mutation to create a wallet address for the account.
The mutation and example JSON request/response is the same as what’s given [above](#create-wallet-addresses-through-a-script).
# Webhook events
The main communication channel between you and your Rafiki instance consists of the Backend Admin API and webhook events sent by the `backend` service. By default, events are sent only to the tenant’s configured webhook URL. The operator can choose to receive webhook events for all tenants by setting the `SEND_TENANT_WEBHOOKS_TO_OPERATOR` environment variable to `true` on the `backend` service.
Most events require you to interact with Rafiki to provide wallet address information or manage (deposit or withdraw) liquidity. This page describes how you should handle each webhook event.
Note
Rafiki doesn’t hold *user* account balances. Instead, Rafiki keeps track of the liquidity in your instance’s asset, peer, and payment accounts in its own database.
## [Specify your webhook endpoint](#specify-your-webhook-endpoint)
[Section titled “Specify your webhook endpoint”](#specify-your-webhook-endpoint)
For Rafiki to notify you about webhook events, you must expose a webhook endpoint that listens for the events dispatched by Rafiki. These events notify your system of time-sensitive status updates, warnings, and errors so that you can react accordingly.
The webhook URL is configured on a per-tenant basis. The operator can configure a tenant-specific webhook URL using the Backend Admin API. If a tenant-specific webhook URL isn’t configured, Rafiki will use the default `WEBHOOK_URL` environment variable.
When an event occurs, the [`backend`](/integration/deployment/services/backend-service) service makes a POST request to your configured webhook endpoint. The `backend` service expects a `200` status in return.
| Variable | Type | Description |
| ------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `WEBHOOK_URL` | `backend` | The default endpoint to where requests are made when a webhook event occurs. This URL is only used if a tenant-specific webhook URL isn’t configured. |
## [Webhook event request body](#webhook-event-request-body)
[Section titled “Webhook event request body”](#webhook-event-request-body)
Each webhook event is sent as a JSON payload with the following structure in the request body. The parameters in the `data` object vary depending on the event.
| Attribute | Type | Description | Required |
| --------- | ------ | --------------------------------------------------- | -------- |
| `id` | String | UUID for the event | Y |
| `type` | Enum | The `EventType` | Y |
| `data` | Object | Additional data that coincides with the `EventType` | Y |
Duplicate events
The `id` in the webhook event payload is unique. Your system can use the ID to determine whether the event was previously received, preventing duplicate event processing.
Expand for example JSON payloads
* incomingPaymentCompleted
```json
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"type": "incoming_payment.created",
"data": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"walletAddressId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"client": "string",
"completed": true,
"incomingAmount": "string",
"receivedAmount": "string",
"metadata": {
"additionalProp1": {}
},
"createdAt": "2024-08-29T08:13:08.966Z",
"updatedAt": "2024-08-29T08:13:08.966Z",
"expiresAt": "2024-08-29T08:13:08.966Z"
}
}
```
* outgoingPaymentCreated
```json
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"type": "outgoing_payment.created",
"data": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"walletAddressId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"client": "string",
"state": "FUNDING",
"receiver": "https://example.com/",
"debitAmount": "string",
"sentAmount": "string",
"metadata": {
"additionalProp1": {}
},
"createdAt": "2024-08-29T11:07:56.090Z",
"updatedAt": "2024-08-29T11:07:56.090Z",
"expiresAt": "2024-08-29T11:07:56.090Z",
"error": "string",
"stateAttempts": 0,
"balance": "string",
"peerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
}
```
* assetLiquidityLow
```json
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"type": "asset.liquidity_low",
"data": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"asset": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"code": "string",
"scale": 0
},
"liquidityThreshold": "string",
"balance": "string"
}
}
```
* walletAddressNotFound
```json
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"type": "wallet_address.not_found",
"data": {
"walletAddressUrl": "string"
}
}
```
We provide an [OpenAPI specification](https://github.com/interledger/rafiki/blob/main/packages/backend/src/openapi/specs/webhooks.yaml) for the webhook events fired by Rafiki.
Additionally, the [Local Playground](/integration/playground/overview) contains example payloads in the [Bruno collection](https://github.com/interledger/rafiki/tree/main/bruno/collections/Rafiki/Sample%20Webhook%20Events) you can use to test your webhook service integration.
## [Verify webhook signatures](#verify-webhook-signatures)
[Section titled “Verify webhook signatures”](#verify-webhook-signatures)
To protect your endpoint from unauthorized or spoofed requests, Rafiki supports an optional, but highly recommended, webhook signature verification process. By enabling signature verification, you can ensure that webhook requests are genuinely from Rafiki, and havn’t been tampered with.
Each webhook request includes a `Rafiki-Signature` header with a timestamp, version, and signature digest. If your instance is configured with both the `SIGNATURE_SECRET` (to generate the signature) and the `SIGNATURE_VERSION` (to set the version, defaults to v1) environment variables, you can verify the authenticity of each webhook request using the steps below.
### [Extract the timestamp and signature from the header](#extract-the-timestamp-and-signature-from-the-header)
[Section titled “Extract the timestamp and signature from the header”](#extract-the-timestamp-and-signature-from-the-header)
The `Rafiki-Signature` header in each webhook request has the following format:
Rafiki-Signature header
```bash
Rafiki-Signature: t=, v=
```
* `t=`: The UNIX timestamp (in seconds) when the signature was generated.
* `v=`: The versioned HMAC SHA-256 signature digest. The default version is `v1`.
### [Prepare the signed payload string](#prepare-the-signed-payload-string)
[Section titled “Prepare the signed payload string”](#prepare-the-signed-payload-string)
To recreate the signed payload string, concatenate the following.
* The timestamp extracted from the header
* A period (.) character
* The actual JSON payload from the request body, containing the `id`, `type`, and `data` attributes
This string format is essential for accurate signature validation. The webhook body must be JSON canonicalized before HMAC computation.
### [Generate the expected signature](#generate-the-expected-signature)
[Section titled “Generate the expected signature”](#generate-the-expected-signature)
Use HMAC SHA-256 with the `SIGNATURE_SECRET` environment variable as the key and the signed payload string as the message.
### [Compare the signatures](#compare-the-signatures)
[Section titled “Compare the signatures”](#compare-the-signatures)
Finally, compare the signature in the header to the expected signature you generated. For security, use a constant-time comparison function to prevent timing attacks.
### [Example](#example)
[Section titled “Example”](#example)
Below is an example in JavaScript to verify Rafiki’s webhook signature:
Verify webhook signature example
```js
function verifyWebhookSignature(request: Request): boolean {
const signatureParts = request.headers['Rafiki-Signature'].split(', ')
const timestamp = signatureParts[0].split('=')[1]
const signatureVersionAndDigest = signatureParts[1].split('=')
const signatureVersion = signatureVersionAndDigest[0].replace('v', '')
const signatureDigest = signatureVersionAndDigest[1]
if (signatureVersion !== config['SIGNATURE_VERSION']) {
return false
}
const payload = `${timestamp}.${canonicalize(request.body)}`
const hmac = createHmac('sha256', config['SIGNATURE_SECRET'])
hmac.update(payload)
const digest = hmac.digest('hex')
return digest === signatureDigest
}
```
To learn how to sign requests you send to the Admin API, see [Signing Admin API requests](/apis/graphql/signing-admin-api-requests).
## [Event handling](#event-handling)
[Section titled “Event handling”](#event-handling)
### [Asynchronous handling](#asynchronous-handling)
[Section titled “Asynchronous handling”](#asynchronous-handling)
If requests to credit/debit user accounts are lengthy processes, we recommend using a worker to process received events. The worker allows the server to process events at a rate suitable for your system and reduces the number of failed/retried events since your event listener can immediately reply with a successful `200` status.
### [Error handling](#error-handling)
[Section titled “Error handling”](#error-handling)
If a non-200 status is returned, indicating an error, or the request times out, Rafiki retries the webhook request at increasing intervals until a `200` status is returned. The first retry occurs after 10 seconds. Additional retries occur after 20 more seconds, then after 30 more seconds, and so on.
| Variable | Type | Description |
| ------------------- | --------- | --------------------------------------------------------------------------------------------------------------- |
| `WEBHOOK_TIMEOUT` | `backend` | The time, in milliseconds, after which a webhook request times out |
| `WEBHOOK_MAX_RETRY` | `backend` | The maximum number of retries for a webhook event when a non-200 status is returned or if the request timed out |
## [Webhook events](#webhook-events)
[Section titled “Webhook events”](#webhook-events)
### [Incoming payments](#incoming-payments)
[Section titled “Incoming payments”](#incoming-payments)
| Event type | Description |
| ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| [`incoming_payment.created`](#incoming-payment-created) | An incoming payment was created |
| [`incoming_payment.completed`](#incoming-payment-completed) | An incoming payment is complete and won’t accept any additional incoming funds |
| [`incoming_payment.partial_payment_received`](#incoming-payment-partial-payment-received) | An incoming payment has received a partial payment |
| [`incoming_payment.expired`](#incoming-payment-expired) | An incoming payment expired and won’t accept any additional incoming funds |
#### [Incoming payment created](#incoming-payment-created)
[Section titled “Incoming payment created”](#incoming-payment-created)
Expand for event sequence
```
sequenceDiagram
participant R as Rafiki
participant ASE as Account servicing entity
R->>ASE: Fires incoming_payment.created event to webhook endpoint
ASE->>ASE: No action required
```
The `incoming_payment.created` event indicates an incoming payment was created. At this point, the incoming payment hasn’t received any funds.
The incoming payment can either complete, receive a partial payment, or expire.
#### [Incoming payment completed](#incoming-payment-completed)
[Section titled “Incoming payment completed”](#incoming-payment-completed)
Expand for event sequences
Single-phase transfer An incoming payment of $10 was completed.
```
sequenceDiagram
participant R as Rafiki
participant ASE as Account servicing entity
R->>ASE: Fires incoming_payment.completed event to webhook endpoint,
receivedAmount: $10
ASE->>R: Backend Admin API call: createIncomingPaymentWithdrawal
R-->>ASE: success: true
ASE->>ASE: Credit recipient's account with $10
```
Two-phase transfer An incoming payment of $10 was completed.
```
sequenceDiagram
participant R as Rafiki
participant ASE as Account servicing entity
R->>ASE: Fires incoming_payment.completed event to webhook endpoint,
receivedAmount: $10
ASE->>R: Backend Admin API call: createIncomingPaymentWithdrawal
R-->>ASE: success: true
ASE->>ASE: Credit recipient's account with $10
ASE->>R: Backend Admin API call: postLiquidityWithdrawal
R-->>ASE: success: true
R->>R: Two-phase transfer completed
```
The `incoming_payment.completed` event indicates the payment completed either automatically or manually, and that any funds received into the incoming payment should be withdrawn and then credited to the recipient’s account on your ledger.
#### [Incoming payment partial payment received](#incoming-payment-partial-payment-received)
[Section titled “Incoming payment partial payment received”](#incoming-payment-partial-payment-received)
Expand for event sequence
```
sequenceDiagram
participant R as Rafiki
participant ASE as Account servicing entity
R->>ASE: Fires incoming_payment.partial_payment_received event to webhook endpoint
ASE->>ASE: Reviews event data
alt Accept partial payment
ASE->>R: Backend Admin API call: ConfirmPartialIncomingPayment
R-->>ASE: success: true
ASE->>ASE: Process payment (e.g. credit receiver)
else Reject partial payment
ASE->>R: Backend Admin API call: RejectPartialIncomingPayment
R-->>ASE: success: true
end
```
The `incoming_payment.partial_payment_received` event indicates an existing incoming payment has received a partial payment. The event should be reviewed for data from the sender as the event could contain details that the ASE should handle before approving the partial payment. The ASE can also choose to reject the partial payment. A reject reason will be transmitted back to the sender in the `outgoing_payment.failed` webhook.
An example use case for this event is approving a specific payment after an AML/KYC check is successful. Partial payment decisioning is disabled by default and can be enabled through the `ENABLE_PARTIAL_PAYMENT_DECISION` environment variable.
| Environment variables | Type | Description |
| ------------------------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ENABLE_PARTIAL_PAYMENT_DECISION` | `backend` | Enables an ASE to act upon (approve/reject) a partial payment. The default is `false`. |
| `DB_ENCRYPTION_SECRET` | `backend` | A base64-encoded secret used to encrypt/decrypt transmitted payment data (`dataToTransmit`) stored on incoming/outgoing payment records and events. |
| `PARTIAL_PAYMENT_DECISION_MAX_WAIT_MS` | `backend` | The maximum time, in milliseconds, Rafiki will wait for an ASE to confirm or reject a partial incoming payment. Rafiki will reject the payment if a response isn’t received in time. Only used if this value is less than the `PARTIAL_PAYMENT_DECISION_SAFETY_MARGIN_MS` value. |
| `PARTIAL_PAYMENT_DECISION_SAFETY_MARGIN_MS` | `backend` | The time, in milliseconds, Rafiki is guaranteed to have to respond before an ILP packet expires. |
#### [Incoming payment expired](#incoming-payment-expired)
[Section titled “Incoming payment expired”](#incoming-payment-expired)
Expand for event sequence
$2.55 was received before the payment expired. The recipient is thus credited with $2.55.
```
sequenceDiagram
participant R as Rafiki
participant ASE as Account servicing entity
R->>ASE: Fires incoming_payment.expired event to webhook endpoint,
receivedAmount: $2.55
ASE->>R: Backend Admin API call: createIncomingPaymentWithdrawal
R-->>ASE: success: true
ASE->>ASE: Credit recipient's account with $2.55
```
The `incoming_payment.expired` event will only fire if funds were received for the incoming payment. The event signals the end of any additional payments.
The primary use case for this event is to know when a streaming payment, such as one supported through [Web Monetization](https://webmonetization.org), has expired. In response to the event, any funds already received for the payment should be withdrawn and credited to the recipient’s account on your ledger.
Note
In some scenarios, a sender may not have specified an `incomingAmount` when the incoming payment was created. Receiving an `incoming_payment.expired` event indicates that no further payments are expected.
### [Outgoing payments](#outgoing-payments)
[Section titled “Outgoing payments”](#outgoing-payments)
| Event type | Description |
| ----------------------------------------------------------- | -------------------------------------------------- |
| [`outgoing_payment.created`](#outgoing-payment-created) | An outgoing payment was created |
| [`outgoing_payment.completed`](#outgoing-payment-completed) | An outgoing payment has completed |
| [`outgoing_payment.failed`](#outgoing-payment-failed) | An outgoing payment partially or completely failed |
#### [Outgoing payment created](#outgoing-payment-created)
[Section titled “Outgoing payment created”](#outgoing-payment-created)
Expand for event sequence
An outgoing payment for $12 was created.
```
sequenceDiagram
participant R as Rafiki
participant ASE as Account servicing entity
R->>ASE: Fires outgoing_payment.created event to webhook endpoint,
debitAmount: $12
ASE->>ASE: Checks that sender's account has sufficient funds
alt Account has sufficient funds
ASE->>ASE: Put hold of $12 on sender's account
ASE->>R: Backend Admin API call: depositOutgoingPaymentLiquidity
R-->>ASE: success: true
else Account has insufficient funds
ASE->>R: Backend Admin API call: cancelOutgoingPayment,
Reason: insufficient funds
R-->>ASE: success: true
end
```
The `outgoing_payment.created` event indicates an outgoing payment was created and is awaiting liquidity. Verify the sender’s account balance and perform any other necessary verifications before funding the payment.
If the sender has insufficient funds or if the payment should otherwise not be fulfilled, cancel the outgoing payment. Otherwise, put a hold on the sender’s account and deposit the funds into Rafiki.
#### [Outgoing payment completed](#outgoing-payment-completed)
[Section titled “Outgoing payment completed”](#outgoing-payment-completed)
Expand for event sequences
Single-phase transfer An outgoing payment for $12 is complete. $11.50 was sent. You choose to keep $0.50 as a service fee.
```
sequenceDiagram
participant R as Rafiki
participant ASE as Account servicing entity
R->>ASE: Fires outgoing_payment.completed event to webhook endpoint,
debitAmount: $12, sentAmount: $11.50
ASE->>R: Backend Admin API call: createOutgoingPaymentWithdrawal
R-->>ASE: success: true
ASE->>ASE: Remove hold and deduct $12 from sender's account,
credit your account with $0.50
```
Two-phase transfer An outgoing payment for $12 is complete. $11.50 was sent. You choose to keep $0.50 as a service fee.
```
sequenceDiagram
participant R as Rafiki
participant ASE as Account servicing entity
R->>ASE: Fires outgoing_payment.completed event to webhook endpoint,
debitAmount: $12, sentAmount: $11.50
ASE->>R: Backend Admin API call: createOutgoingPaymentWithdrawal
R-->>ASE: success: true
ASE->>ASE: Remove hold and deduct $12 from sender's account,
credit your account with $0.50
ASE->>R: Backend Admin API call: postLiquidityWithdrawal
R-->>ASE: success: true
R->>R: Two-phase transfer complete
```
The `outgoing_payment.completed` event indicates that as much as possible was sent to the recipient against their incoming payment.
If there is excess liquidity in Rafiki due to differences between the sent and received amounts, withdraw the excess from the outgoing payment. What you choose to do with the excess is a business decision. One option is to return the excess to the sender. Another option is to retain the excess as a service fee. Lastly, remove the hold on your sender’s account and debit their account on your ledger.
#### [Outgoing payment failed](#outgoing-payment-failed)
[Section titled “Outgoing payment failed”](#outgoing-payment-failed)
Expand for event sequence
An outgoing payment for $12 failed. $8 was sent successfully.
```
sequenceDiagram
participant R as Rafiki
participant ASE as Account servicing entity
R->>ASE: Fires outgoing_payment.failed event to webhook endpoint,
debitAmount: $12, sentAmount: $8
ASE->>R: Backend Admin API call: createOutgoingPaymentWithdrawal
R-->>ASE: success: true
ASE->>ASE: Remove hold and deduct $8 from the sender's account
```
The `outgoing_payment.failed` event indicates that an outgoing payment has either partially or completely failed and a retry was unsuccessful. Withdraw any remaining liquidity from the outgoing payment in Rafiki. If the payment failed completely (the `sentAmount` is `0`), remove the hold from your sender’s account. If the payment partially failed, remove the hold from your sender’s account, then debit the sender’s account on your ledger with the amount that was sent successfully. Since there will be a discrepancy between the quoted amount and the actual sent amount, we suggest you refrain from taking a sending fee.
### [Wallet addresses](#wallet-addresses)
[Section titled “Wallet addresses”](#wallet-addresses)
| Event type | Description |
| --------------------------------------------------------------------- | ----------------------------------------------------------------- |
| [`wallet_address.not_found`](#wallet-address-not-found) | The requested wallet address wasn’t found on this Rafiki instance |
| [`wallet_address.web_monetization`](#wallet-address-web-monetization) | Web Monetization payments have been received via STREAM |
#### [Wallet address not found](#wallet-address-not-found)
[Section titled “Wallet address not found”](#wallet-address-not-found)
Expand for event sequence
The wallet address, `https://wallet.example.com/carla_garcia` was requested but doesn’t exist.
```
sequenceDiagram
participant R as Rafiki
participant ASE as Account servicing entity
R->>ASE: Fires wallet_address.not_found event to webhook endpoint,
wallet address: https://wallet.example.com/carla_garcia
ASE->>R: Backend Admin API call: createWalletAddress,
url: https://wallet.example.com/carla_garcia,
public name: Carla Eva Garcia
R-->>ASE: success: true
```
The `wallet_address.not_found` event indicates that a wallet address was requested via the Open Payments GET wallet address API call, but the address doesn’t exist in your Rafiki instance.
When you receive this event, look up the associated account in your system and create a wallet address for the account. The initial wallet address request will succeed if you create it in your configured `WALLET_ADDRESS_LOOKUP_TIMEOUT_MS` time frame.
| Environment variable | Type | Description |
| ---------------------------------- | --------- | -------------------------------------------------------------------------------------------------------------- |
| `WALLET_ADDRESS_LOOKUP_TIMEOUT_MS` | `backend` | The time in milliseconds that you have to create a missing wallet address before the initial request times out |
#### [Wallet address Web Monetization](#wallet-address-web-monetization)
[Section titled “Wallet address Web Monetization”](#wallet-address-web-monetization)
Expand for event sequence
A wallet address received a Web Monetization payment of $0.33
```
sequenceDiagram
participant R as Rafiki
participant ASE as Account servicing entity
R->>ASE: Fires wallet_address.web_monetization event to webhook endpoint,
receivedAmount: $0.33
ASE->>R: Backend Admin API call: createWalletAddressWithdrawal
R-->>ASE: success: true
ASE->>ASE: Credit recipient's account with $0.33
```
The `wallet_address.web_monetization` event indicates that a wallet address received Web Monetization payments via the ILP STREAM protocol. Withdraw the liquidity from the wallet address in Rafiki and credit the recipient’s account on your ledger.
### [Low asset liquidity](#low-asset-liquidity)
[Section titled “Low asset liquidity”](#low-asset-liquidity)
| Event type | Description |
| --------------------------------------------- | ------------------------------------------------------------- |
| [`asset.liquidity_low`](#asset-liquidity-low) | Your asset liquidity has dropped below your defined threshold |
#### [Asset liquidity low](#asset-liquidity-low)
[Section titled “Asset liquidity low”](#asset-liquidity-low)
Expand for event sequence
Your asset liquidity for USD (asset scale: 2) drops below $100.00.
```
sequenceDiagram
participant R as Rafiki
participant ASE as Account servicing entity
R->>ASE: Fires asset.liquidity_low event to webhook endpoint,
asset: USD (scale: 2, id: "abc")
ASE->>R: Backend Admin API call: depositAssetLiquidity
R-->>ASE: success: true
```
The `asset.liquidity_low` event indicates that an asset’s liquidity has dropped below your predefined liquidity threshold. Check if you already have, or can acquire, additional liquidity for that specific asset. If so, deposit it in Rafiki. Cross-currency transfers will fail if you don’t increase the asset’s liquidity.
### [Low peer liquidity](#low-peer-liquidity)
[Section titled “Low peer liquidity”](#low-peer-liquidity)
| Event type | Description |
| ------------------------------------------- | ------------------------------------------------------------ |
| [`peer.liquidity_low`](#peer-liquidity-low) | Your peer liquidity has dropped below your defined threshold |
#### [Peer liquidity low](#peer-liquidity-low)
[Section titled “Peer liquidity low”](#peer-liquidity-low)
Expand for event sequence
The liquidity for your peer, Happy Life Bank, drops below $100.00 USD.
```
sequenceDiagram
participant R as Rafiki
participant ASE as Account servicing entity
R->>ASE: Fires peer.liquidity_low event to webhook endpoint,
peer: Happy Life Bank (asset: "USD", scale: 2, id: "abc")
ASE->>R: Backend Admin API call: depositPeerLiquidity
R-->>ASE: success: true
```
The `peer.liquidity_low` event indicates that a peer’s liquidity has dropped below your predefined liquidity threshold. Decide whether you want to extend the peer’s credit line or if your peer must settle before you will extend a new line of credit. If you can’t or don’t increase the peer liquidity in Rafiki, transfers to that peer will fail.
# Account servicing entity (ASE)
An account servicing entity (ASE) is a regulated entity that provides and maintains payment accounts for its customers. Examples of ASEs include banks, digital wallet providers, and mobile money providers.
As regulated entities, ASEs are subject to the laws, rules, and regulations of their jurisdictions. As such, Rafiki should **not** be used in production environments by non-regulated entities.
## [Responsibilities and obligations](#responsibilities-and-obligations)
[Section titled “Responsibilities and obligations”](#responsibilities-and-obligations)

### [AML (Anti-money laundering)](#aml-anti-money-laundering)
[Section titled “AML (Anti-money laundering)”](#aml-anti-money-laundering)
ASEs follow anti-money laundering laws and regulations to detect and prevent money laundering and other suspicious financial activities.
### [KYC/KYB (Know your customer/business)](#kyckyb-know-your-customerbusiness)
[Section titled “KYC/KYB (Know your customer/business)”](#kyckyb-know-your-customerbusiness)
KYC and KYB practices ensure ASEs verify the identities of their customers by collecting IDs and proof of address, verifying business registrations, checking against sanctions lists, and other processes.
### [User account management](#user-account-management)
[Section titled “User account management”](#user-account-management)
ASEs manage the creation, upkeep, and security of their customers’ accounts and balances therein. Similarly, they’re responsible for authenticating their customers and providing secure channels for them to interact with their accounts via mobile apps, websites, or other interfaces.
### [Ledger](#ledger)
[Section titled “Ledger”](#ledger)
As ASEs handle deposits and withdrawals through external payment methods (like bank transfers, credit cards, and other services) they must record all transactions and balance information in their ledger.
# Accounting in Rafiki
Rafiki uses [double-entry accounting](https://en.wikipedia.org/wiki/Double-entry_bookkeeping) to record financial transactions. In this method of bookkeeping, a transaction recorded to one account results in an equal and opposite entry to another account. For example, a $50 credit to one account results in a $50 debit from another account.
Transactions in Rafiki represent Interledger packet interactions, denominated in a given [asset](#assets). Packet interactions can be successful, fail, or be rejected. Rafiki’s accounting layer processes the interactions and converts the activities into financial records, which are then written to your [accounting database](#accounting-databases).
Accounts in Rafiki are your internal [liquidity](#liquidity-accounts) and [settlement](#settlement-accounts) accounts used to fund payments, not the accounts that you service for your customers. This distinction is crucial for understanding how Rafiki handles transactions and settlements.
## [Assets](#assets)
[Section titled “Assets”](#assets)
An asset represents a transferrable item of value. Although the Interledger Protocol (ILP) supports the transfer of any asset deemed to have value, assets are generally denominated in a currency. For example fiat currencies, central bank digital currencies, and branded currencies (such as merchant reward points).
Part of Rafiki’s [integration requirements](/integration/requirements/assets) include adding one or more assets that you support.
An asset is made up of the following properties.
| Property | Type | Description | Example |
| ------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------ | ------- |
| `value` | BigInt | A numerical amount | `10000` |
| `assetCode` | String | A code representing the asset. An [ISO 4217 currency code](https://en.wikipedia.org/wiki/ISO_4217) should be used whenever possible. | `"USD"` |
| `assetScale` | Integer | Difference in order of magnitude between the standard unit and a fractional unit | `2` |
To convert an asset’s value into an amount that’s easier to interpret, apply the following formula.
\= *currencyAmount*
Using the example data from the preceding table, the formula looks like this:
100.00 USD
## [Accounts](#accounts)
[Section titled “Accounts”](#accounts)
Rafiki uses a combination of liquidity and settlement accounts to track the amounts available to fund transactions. Rafiki doesn’t physically hold funds in each account. Instead, it uses [double-entry accounting](https://en.wikipedia.org/wiki/Double-entry_bookkeeping) to record the transactions. The actual settlement of amounts owed, in which funds are physically exchanged, occurs outside of both Rafiki and the Interledger Protocol.
### [Liquidity accounts](#liquidity-accounts)
[Section titled “Liquidity accounts”](#liquidity-accounts)
Liquidity accounts track deposits, withdrawals, and transfers that occur during the course of a transaction. Rafiki provides liquidity accounts for assets, peers, and payments.
Liquidity accounts hold either a zero or a positive balance. Rafiki ensures that the total debits to a liquidity account won’t exceed the account’s total credits.
| Account type | What the account represents | Number of accounts |
| ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- | ------------------------ |
| [Asset liquidity](#asset-liquidity-accounts) | The value, denominated in a given asset, that Rafiki has available to support cross-currency transactions | One per asset |
| [Peer liquidity](#peer-liquidity-accounts) | The credit line, denominated in the asset of your peering relationship, that you extend to a peer | One per peer |
| [Incoming payment liquidity](#incoming-payment-liquidity-accounts) | The value received from a completed incoming payment | One per incoming payment |
| [Outgoing payment liquidity](#outgoing-payment-liquidity-accounts) | The value that Rafiki will attempt to send in an outgoing payment | One per outgoing payment |
| [Wallet address liquidity](#wallet-address-liquidity-accounts) | The value that a wallet address received via SPSP | One per wallet address |
#### [Asset liquidity accounts](#asset-liquidity-accounts)
[Section titled “Asset liquidity accounts”](#asset-liquidity-accounts)
Asset liquidity ensures Rafiki has enough liquidity, denominated in a given asset, to handle cross-currency (foreign exchange) transactions.
An asset liquidity account represents the value that Rafiki has available for sending or forwarding ILP packets. You have one asset liquidity account for each asset you transact in.
Whenever an outgoing payment/incoming payment is in a different asset than the peering relationship, the liquidity of asset accounts change depending on the FX direction. Any transaction that would result in a negative balance will fail.
Note
If you and your peer transact in the same asset (there’s no currency conversion) and you both provide your customers only with wallet addresses denominated in that asset, then there will be no movement into/from the corresponding asset’s liquidity account.
For example, you and your peer transact in USD and only provide your customers with USD wallet addresses. One of your customers sends $10 to your peer’s customer. There’s no movement from your USD asset liquidity account because there was no currency conversion. There is, however, an [outgoing payment liquidity account](/overview/concepts/accounting#outgoing-payment-liquidity-accounts) created to support the transaction.
You can add a liquidity threshold for each asset liquidity account via the [`updateAsset`](https://rafiki.dev/apis/graphql/backend/mutations/#updateasset) mutation’s `liquidityThreshold` input argument.
When a threshold is entered, the [`asset.liquidity_low`](/integration/requirements/webhook-events#asset-liquidity-low) webhook event notifies you if an asset account’s liquidity drops below the threshold.
You should define and adjust asset liquidity based on your liquidity risk profile. You can deposit or withdraw asset liquidity as needed through [Rafiki Admin](/admin/admin-user-guide#edit-asset) or by using the [Backend Admin API](/admin/liquidity/asset-liquidity#manage-asset-liquidity-using-the-backend-admin-api).
Asset liquidity example - cross-currency transactions
Your Rafiki instance is configured for two assets: EUR and USD.
* Rafiki holds an asset liquidity account for both EUR and USD.
* You’ve set the asset scale of both currencies to 0.
* Your starting EUR liquidity is 10 and your USD liquidity is 50.
**Cross-currency transaction #1:**
1. Rafiki receives packets from a peer. These packets are all denominated in EUR, worth €10. 10 EUR move from the peer’s liquidity account on your Rafiki instance to your EUR asset liquidity account. Your EUR liquidity increases to 20 (10 + 10).
2. The EUR-to-USD exchange rate is applied, with €10 equating to $12 USD. Since your starting USD liquidity is 50, your USD asset liquidity account can cover the transfer of $12 USD to an incoming payment liquidity account. Your USD liquidity decreases to 38 (50 - 12).
**Cross-currency transaction #2:**
1. Rafiki receives packets from a peer. These packets are all denominated in EUR, worth €50. Your EUR liquidity increases to 70 (20 + 50).
2. The current EUR-to-USD exchange rate is applied, with €50 equating to $55 USD. The transaction fails. Your USD liquidity account is 38, so you don’t have enough liquidity to cover the transaction.
3. Your EUR liquidity reduces back to 20 (70 - 50).
#### [Peer liquidity accounts](#peer-liquidity-accounts)
[Section titled “Peer liquidity accounts”](#peer-liquidity-accounts)
Peer liquidity is the credit line you’ve extended to a peer. A peer liquidity account represents the amount of the line of credit that the peer still has available to them. You have one liquidity account for each peer and the account is denominated in the asset you both agreed to transact in.
The amount of credit that you extend to a peer, the asset that you transact in, and the mechanism you use to settle are just a few items that should be defined in your respective peering agreements.
Note
A peering agreement is a legal contract between the parties involved in a peering relationship. It defines terms such as the assets involved and other operational details. It’s not configured or managed in Rafiki but is necessary for establishing the terms under which assets are exchanged.
If a peer’s liquidity is insufficient (for example, they’ve used up their allotted credit line), payments won’t be processed. Your peer should settle with you so that you can reset their liquidity.
You can add a liquidity threshold for each peer liquidity account via the [`updatePeer`](https://rafiki.dev/apis/graphql/backend/mutations/#updatepeer) mutation’s `liquidityThreshold` input argument.
When a threshold is entered, the [`peer.liquidity_low`](/integration/requirements/webhook-events#peer-liquidity-low) webhook event notifies you if a peer’s liquidity drops below the threshold.
You should define and adjust each peer’s liquidity based on your liquidity risk profile. You can deposit or withdraw peer liquidity as needed through [Rafiki Admin](/admin/admin-user-guide#edit-peer) or by using the [Backend Admin API](/admin/liquidity/peer-liquidity#manage-peer-liquidity-using-the-backend-admin-api).
Peer liquidity example
You and Cloud Nine Wallet are peers. You’ve agreed to extend Cloud Nine Wallet a line of credit worth $100.00 USD. This means Cloud Nine Wallet has $100.00 in their peer liquidity account on your Rafiki instance. Your Rafiki instance can receive packets that total up to $100.00 from Cloud Nine Wallet. When the $100.00 is used up, Cloud Nine Wallet settles with you by sending $100.00 via the shared settlement mechanism outlined in your peering agreement. When you receive the funds, you reset their liquidity in Rafiki.
#### [Payment liquidity accounts](#payment-liquidity-accounts)
[Section titled “Payment liquidity accounts”](#payment-liquidity-accounts)
Payment liquidity is the amount that’s available because of an incoming or outgoing payment. Rafiki has three types of payment liquidity accounts.
| Payment type | Purpose |
| ---------------------------------------------------- | ------------------------------------------------------- |
| [Incoming](#incoming-payment-liquidity-accounts) | For incoming payments created via the Open Payments API |
| [Outgoing](#outgoing-payment-liquidity-accounts) | For outgoing payments created via the Open Payments API |
| [Wallet address](#wallet-address-liquidity-accounts) | For payments sent via SPSP |
##### [Incoming payment liquidity accounts](#incoming-payment-liquidity-accounts)
[Section titled “Incoming payment liquidity accounts”](#incoming-payment-liquidity-accounts)
An incoming payment liquidity account represents the value received for an incoming payment. Incoming payments are created via [Open Payments](https://openpayments.dev/apis/resource-server/operations/create-incoming-payment/). When the first packet for an incoming payment is received, a corresponding liquidity account is automatically created. You will have one liquidity account per incoming payment.
You are notified of created, completed, and expired incoming payments by listening for the appropriate [webhook events](/integration/requirements/webhook-events/#incoming-payments). Since Rafiki doesn’t hold funds, anything you receive must be withdrawn and then credited to the recipient’s account on your ledger.
The liquidity account isn’t used again after the payment completes, but its record remains in your accounting database. When a new incoming payment occurs, a new liquidity account is created.
##### [Outgoing payment liquidity accounts](#outgoing-payment-liquidity-accounts)
[Section titled “Outgoing payment liquidity accounts”](#outgoing-payment-liquidity-accounts)
An outgoing payment liquidity account represents the value available to send in an outgoing payment. When an outgoing payment is created via [Open Payments](https://openpayments.dev/apis/resource-server/operations/create-outgoing-payment/), a corresponding liquidity account is automatically created. You will have one liquidity account per outgoing payment.
You are notified of created, completed, and failed outgoing payments by listening for the appropriate [webhook events](/integration/requirements/webhook-events/#outgoing-payments). Liquidity must be deposited into the outgoing payment account before the payment can be processed.
You may occasionally have excess liquidity, such as when an outgoing payment only partially completes and a portion of the send-amount remains. Since Rafiki doesn’t hold funds, any excess liquidity that remains after an outgoing payment completes must be withdrawn from the outgoing payment liquidity account. How you choose to handle the excess is up to you. You could, for example, refund the excess to the sender or take the amount as a fee.
The account isn’t used again after the payment completes, but its record remains in your accounting database. When a new outgoing payment is created, a new liquidity account is created.
##### [Wallet address liquidity accounts](#wallet-address-liquidity-accounts)
[Section titled “Wallet address liquidity accounts”](#wallet-address-liquidity-accounts)
A wallet address liquidity account contains the value received to a wallet address via [SPSP](/overview/concepts/interledger#simple-payment-setup-protocol-spsp). When an incoming payment is created, a corresponding liquidity account is automatically created. You will have one account per wallet address.
Since Rafiki doesn’t hold funds, you must withdraw the liquidity when the payment completes and credit the funds to the recipient’s account on your ledger. You are notified to withdraw liquidity by listening for the appropriate [webhook event](/integration/requirements/webhook-events#wallet-addresses).
Unlike the incoming and outgoing payment liquidity accounts, the same wallet address liquidity account will be used for future incoming SPSP payments.
### [Settlement accounts](#settlement-accounts)
[Section titled “Settlement accounts”](#settlement-accounts)
A settlement account represents the total funds, denominated in a single asset, that you have deposited into Rafiki. You have one settlement account for each asset you transact in.
Settlement accounts hold either a zero or a negative balance. A negative balance on a settlement account means you’ve deposited more funds into Rafiki than you’ve withdrawn. The closer a settlement account’s balance is to 0, the more likely it is you need to settle with your peer for the amount owed and then deposit the amount back into Rafiki.
Rafiki ensures that the total credits to a settlement account don’t exceed its total debits.
Settlement account example
You deposit $10,000 into a peer’s liquidity account, meaning you’ve extended a credit line of $10,000 to your peer.
Your peer liquidity account balance is $10,000 and your USD settlement account balance is now -$10,000.
An incoming payment from your peer for $100 is created, meaning your peer is using $100 of their line of credit. Since Rafiki doesn’t hold funds, you must withdraw the liquidity and credit the amount to the recipient’s account on your ledger.
Now, your peer liquidity account’s balance is $9,900 and your USD settlement account’s balance is -$9,900.
## [Accounting databases](#accounting-databases)
[Section titled “Accounting databases”](#accounting-databases)
### [TigerBeetle](#tigerbeetle)
[Section titled “TigerBeetle”](#tigerbeetle)
TigerBeetle is a high-performance distributed financial accounting database used by Rafiki’s [backend service](/integration/deployment/services/backend-service) to store account balance data. Both liquidity and settlement accounts in Rafiki correspond to TigerBeetle credit and debit accounts, respectively.
TigerBeetle only holds balance data without any additional ILP packet metadata. For detailed information on TigerBeetle, including its consensus mechanism and its limitations, visit the official TigerBeetle [documentation](https://docs.tigerbeetle.com/) and [blog](https://tigerbeetle.com/blog/). For more information about TigerBeetle in a production Rafiki environment, see [Running Rafiki in production](/integration/deployment/helm-k8s).
### [Postgres](#postgres)
[Section titled “Postgres”](#postgres)
You can choose to use a separate Postgres database for accounting instead of using TigerBeetle. However, TigerBeetle is recommended due to its speed, efficiency, and dedicated design for handling double-entry/double-ledger accounting.
## [Transfers](#transfers)
[Section titled “Transfers”](#transfers)
As with the accounts described above, Rafiki performs [double-entry accounting](https://en.wikipedia.org/wiki/Double-entry_bookkeeping) for transfers, where increasing the total debits of one account increases the total credits of another account by the same amount, and vice versa.
Transfers can complete in either a single phase or in two phases.
### [Single-phase transfer](#single-phase-transfer)
[Section titled “Single-phase transfer”](#single-phase-transfer)
A single-phase transfer posts funds to accounts immediately when the transfer is created.
**Example of successful single-phase incoming payment**
```
sequenceDiagram
participant R as Rafiki
participant ASE as Account servicing entity
R->>ASE: Fires webhook event when incoming payment completes
ASE->>R: Withdraws payment amount from incoming payment liquidity account
ASE->>ASE: Credits the recipient's account by the payment amount
```
### [Two-phase transfer](#two-phase-transfer)
[Section titled “Two-phase transfer”](#two-phase-transfer)
A two-phase transfer moves funds in two stages.
1. Reserve funds (`pending`)
2. Resolve funds (`post`, `void`, or `expire`)
**Example of successful two-phase incoming payment**
```
sequenceDiagram
Rafiki->>ASE: Fires webhook event when incoming payment completes
ASE->>Rafiki: Withdraws payment amount from incoming payment
liquidity account (reserve funds pending)
ASE->>ASE: Credits the recipient's account by the payment amount
ASE->>Rafiki: Resolve funds (post)
Rafiki->>Rafiki: Two-phase transfer complete
```
The name two-phase transfer is a reference to the [two-phase commit protocol](https://en.wikipedia.org/wiki/Two-phase_commit_protocol) for distributed transactions.
You can [post and commit](/admin/liquidity/two-phase-transfers#post-and-commit-a-successful-transfer) a successful two-phase transfer and [void and roll back](/admin/liquidity/two-phase-transfers#void-and-roll-back-an-unsuccessful-transfer) an unsuccessful two-phase transfer by using the Backend Admin API.
### [Intra-Rafiki transfer examples](#intra-rafiki-transfer-examples)
[Section titled “Intra-Rafiki transfer examples”](#intra-rafiki-transfer-examples)
Remember that a settlement account will always have a zero or negative balance and a liquidity account will always have a zero or positive balance.
* [Deposits](#deposits)
* [Withdrawals](#withdrawals)
* [Payments in the same asset](#payments-in-the-same-asset)
* [Cross currency payments](#cross-currency-payments)
#### [Deposits](#deposits)
[Section titled “Deposits”](#deposits)
A deposit is the act of debiting the settlement account and crediting the liquidity account.
**Example:** Depositing `100 USD` asset liquidity
| Debit Account | Credit Account |
| ------------- | --------------- |
| Settlement | Asset liquidity |
| USD settlement acct | USD asset liquidity acct |
| --------------------- | ------------------------ |
| Debit Credit
100 | Debit Credit
100 |
**Example:** Depositing `100 USD` peer liquidity
| Debit Account | Credit Account |
| ------------- | -------------- |
| Settlement | Peer liquidity |
| USD settlement acct | USD peer liquidity acct |
| --------------------- | ----------------------- |
| Debit Credit
100 | Debit Credit
100 |
**Example:** Depositing `35 USD` outgoing payment liquidity
| Debit Account | Credit Account |
| ------------- | ---------------- |
| Settlement | Outgoing payment |
| USD settlement acct | USD outgoing payment liquidity acct |
| -------------------- | ----------------------------------- |
| Debit Credit
35 | Debit Credit
35 |
#### [Withdrawals](#withdrawals)
[Section titled “Withdrawals”](#withdrawals)
A withdrawal is the act of debiting the liquidity account and crediting the settlement account.
**Example:** Withdrawing `50 USD` in asset liquidity
| Debit Account | Credit Account |
| --------------- | -------------- |
| Asset liquidity | Settlement |
| USD asset liquidity acct | USD settlement acct |
| ------------------------ | ------------------- |
| Debit Credit
50 | Debit Credit
50 |
**Example:** Withdrawing `50 USD` in peer liquidity
| Debit Account | Credit Account |
| -------------- | -------------- |
| Peer liquidity | Settlement |
| USD peer liquidity acct | USD settlement acct |
| ----------------------- | ------------------- |
| Debit Credit
50 | Debit Credit
50 |
**Example:** Withdrawing `2 USD` in wallet address liquidity
| Debit Account | Credit Account |
| -------------- | -------------- |
| Wallet address | Settlement |
| USD wallet address liquidity acct | USD settlement acct |
| --------------------------------- | ------------------- |
| Debit Credit
2 | Debit Credit
2 |
**Example:** Withdrawing `2 USD` in incoming payment liquidity
| Debit Account | Credit Account |
| ---------------- | -------------- |
| Incoming payment | Settlement |
| USD incoming payment liquidity acct | USD settlement acct |
| ----------------------------------- | ------------------- |
| Debit Credit
25 | Debit Credit
25 |
**Example:** Withdrawing `1 USD` in outgoing payment liquidity
| Debit Account | Credit Account |
| ---------------- | -------------- |
| Outgoing payment | Settlement |
| USD outgoing payment liquidity acct | USD settlement acct |
| ----------------------------------- | ------------------- |
| Debit Credit
1 | Debit Credit
1 |
#### [Payments in the same asset](#payments-in-the-same-asset)
[Section titled “Payments in the same asset”](#payments-in-the-same-asset)
**Example:** Sender consented to a payment of `14 USD` but the quote promised to deliver `15 USD`. The send amount is less than the receive amount.
| Debit Account | Credit Account |
| ---------------- | ---------------- |
| Outgoing payment | Incoming payment |
| Asset liquidity | Incoming payment |
| USD outgoing payment liquidity acct | USD asset liquidity acct | USD incoming payment liquidity acct |
| ----------------------------------- | ------------------------ | ----------------------------------- |
| Debit Credit
14 | Debit Credit
1 | Debit Credit
15 |
**Example:** Sender consented to a payment of `15 USD` but the quote promised to deliver `14 USD`. The send amount is more than the receive amount.
| Debit Account | Credit Account |
| ---------------- | ---------------- |
| Outgoing payment | Incoming payment |
| Outgoing payment | Asset liquidity |
| USD outgoing payment liquidity acct | USD asset liquidity acct | USD incoming payment liquidity acct |
| ----------------------------------- | ------------------------ | ----------------------------------- |
| Debit Credit
15 | Debit Credit
1 | Debit Credit
14 |
#### [Cross currency payments](#cross-currency-payments)
[Section titled “Cross currency payments”](#cross-currency-payments)
**Example:** Outgoing payment is for `10 USD`, incoming payment receives `9 EUR` after a currency exchange
| Debit Account | Credit Account | Asset |
| ---------------- | ---------------- | ----- |
| Outgoing payment | Asset liquidity | `USD` |
| Asset liquidity | Incoming payment | `EUR` |
| USD outgoing payment liquidity acct | USD asset liquidity acct |
| ----------------------------------- | ----------------------------------- |
| Debit Credit
10 | Debit Credit
10 |
| EUR asset liquidity acct | EUR incoming payment liquidity acct |
| Debit Credit
9 | Debit Credit
9 |
### [Interledger transfer examples](#interledger-transfer-examples)
[Section titled “Interledger transfer examples”](#interledger-transfer-examples)
In these examples, the sender and receiver don’t have wallet addresses at the same Rafiki instance.
Remember that a settlement account will always have a zero or negative balance and a liquidity account will always have a zero or positive balance.
* [Sending connector - same asset](#sending-connector---same-asset)
* [Sending connector - cross currency](#sending-connector---cross-currency)
* [Receiving connector - same asset](#receiving-connector---same-asset)
* [Receiving connector - cross currency](#receiving-connector---cross-currency)
* [Connector - same asset](#connector---same-asset)
* [Connector - cross currency](#connector---cross-currency)
#### [Sending connector - same asset](#sending-connector---same-asset)
[Section titled “Sending connector - same asset”](#sending-connector---same-asset)
**Example:** Sender creates an outgoing payment for `100 USD` to an incoming payment in the same asset at a peer’s Rafiki instance
| Debit Account | Credit Account |
| ---------------- | -------------- |
| Outgoing payment | Peer liquidity |
| USD outgoing payment liquidity acct | USD peer liquidity acct |
| ----------------------------------- | ----------------------- |
| Debit Credit
100 | Debit Credit
100 |
#### [Sending connector - cross currency](#sending-connector---cross-currency)
[Section titled “Sending connector - cross currency”](#sending-connector---cross-currency)
**Example:** Sender creates an outgoing payment for `100 USD` to an incoming payment at a peer’s Rafiki instance. The peering relationship is in EUR, so the payment is converted on the sending side.
| Debit Account | Credit Account | Asset |
| ---------------- | --------------- | ----- |
| Outgoing payment | Asset liquidity | `USD` |
| Asset Liquidity | Peer Liquidity | `EUR` |
| USD outgoing payment liquidity acct | USD asset liquidity acct |
| ----------------------------------- | ------------------------ |
| Debit Credit
100 | Debit Credit
100 |
| EUR asset liquidity acct | EUR peer liquidity acct |
| Debit Credit
90 | Debit Credit
90 |
#### [Receiving connector - same asset](#receiving-connector---same-asset)
[Section titled “Receiving connector - same asset”](#receiving-connector---same-asset)
**Example:** An incoming payment receives `100 USD` from an outgoing payment in the same asset at a peer’s Rafiki instance.
| Debit Account | Credit Account |
| -------------- | ---------------- |
| Peer liquidity | Incoming payment |
| USD peer liquidity acct | USD incoming payment liquidity acct |
| ----------------------- | ----------------------------------- |
| Debit Credit
100 | Debit Credit
100 |
#### [Receiving connector - cross currency](#receiving-connector---cross-currency)
[Section titled “Receiving connector - cross currency”](#receiving-connector---cross-currency)
**Example:** A Rafiki instance receives `10 USD` from a peer (peering relationship in USD) to be deposited in an incoming payment liquidity account denominated in EUR. The payment is converted to EUR and deposited.
| Debit Account | Credit Account | Asset |
| --------------- | ---------------- | ----- |
| Peer liquidity | Asset liquidity | `USD` |
| Asset liquidity | Incoming payment | `EUR` |
| USD peer liquidity acct | USD asset liquidity acct |
| ------------------------ | ----------------------------------- |
| Debit Credit
10 | Debit Credit
10 |
| EUR asset liquidity acct | EUR incoming payment liquidity acct |
| Debit Credit
9 | Debit Credit
9 |
#### [Connector - same asset](#connector---same-asset)
[Section titled “Connector - same asset”](#connector---same-asset)
**Example:** Rafiki forwards `10 USD` from peer A to peer B.
| Debit Account | Credit Account |
| -------------- | -------------- |
| Peer liquidity | Peer liquidity |
| USD peer “A” liquidity acct | USD peer “B” liquidity acct |
| --------------------------- | --------------------------- |
| Debit Credit
10 | Debit Credit
10 |
#### [Connector - cross currency](#connector---cross-currency)
[Section titled “Connector - cross currency”](#connector---cross-currency)
**Example:** Rafiki receives `100 USD` from peer A and forwards `90 EUR` to peer B.
| Debit Account | Credit Account | Asset |
| --------------- | --------------- | ----- |
| Peer liquidity | Asset liquidity | `USD` |
| Asset liquidity | Peer liquidity | `EUR` |
| USD peer “A” liquidity acct | USD asset liquidity acct |
| --------------------------- | --------------------------- |
| Debit Credit
100 | Debit Credit
100 |
| EUR asset liquidity acct | EUR peer “B” liquidity acct |
| Debit Credit
90 | Debit Credit
90 |
# Clearing and settlement
## [Clearing](#clearing)
[Section titled “Clearing”](#clearing)
When a payment is made over traditional banking rails, the money doesn’t move instantly. First, there are checks to confirm that the money exists and can be transferred. Clearing networks are responsible for exchanging messages between ASEs to facilitate these checks. This process is called clearing. When a payment successfully clears, it means the payer’s ASE has an obligation to the payee’s ASE.
The [Interledger Protocol (ILP)](/overview/concepts/interledger) isn’t a traditional clearing network, but does function in a similar way.
* ASEs that implement the protocol must become [peers](/integration/requirements/peers) to transact with one another. This is comparable to traditional banking, where ASEs must use the same clearing network. An ASE can’t use Interledger to transact with another ASE unless they have both implemented the protocol and have peered with one another.
* Peered ASEs exchange ILP packets, which are packets of value that contain transaction information. ILP packets are akin to the messages exchanged during the traditional clearing process.
* The successful exchange of ILP packets between peers creates obligations between them that must be settled. The receipt of a fulfilled ILP packet is basically a conditional IOU—a promise to pay—that affects the financial accounting balances between the peers.
You can read more about clearing as it relates to ILP in the [Interledger developer docs](https://interledger.org/developers/rfcs/peering-clearing-settling/).
Conceptually, Rafiki sits at the clearing level, but isn’t a clearing network. It’s software that makes implementing the Interledger protocol faster and easier. Rafiki uses ILP to [track liquidity](/overview/concepts/accounting) between assets, payments, and peers. An ASE must still connect Rafiki to their existing backend system and internal ledger for authentication, fetching exchange rates, and managing liquidity itself. For example, if an incoming payment completes in Rafiki, the ASE’s backend must credit the recipient’s account on their own system, however that might look.
In any case, no movement of actual money has occurred yet.
## [Settlement](#settlement)
[Section titled “Settlement”](#settlement)
In traditional banking, settlement is the fulfillment of an obligation between ASEs. It turns the promise of payment into a real payment by moving actual money. This occurs over a shared settlement network, such as Fedwire in the United States.
When a payer’s ASE settles with the payee’s ASE, there’s a high chance that the ASE isn’t physically handing over cash. There’s more likely to be an intermediary, like a reserve bank or central bank, that maintains accounts for both ASEs. The intermediary moves funds from one account to the other, crediting and debiting the accounts as necessary.
With Interledger, the concept of settlement isn’t that different. Each [peer](/integration/requirements/peers) must agree on a settlement system to use to fulfill their obligations with one another. However, ILP itself isn’t a settlement system. This means peers must have some other way to fulfill their obligations and exchange value. Examples can include using a real-time gross settlement system like Fedwire, an automated clearing house (ACH) network, a money transfer service, or some other payment channel. You can read more about settling as it relates to ILP in the [Interledger developer docs](https://interledger.org/developers/rfcs/settlement-engines/).
Rafiki is also not a settlement system. It [keeps track of liquidity](/overview/concepts/accounting) through liquidity and settlement accounts. Liquidity accounts track deposit, withdrawal, and transfer amounts. Settlement accounts track the availability of funds, denominated in a single asset. A negative balance means funds are available. The closer a settlement account’s balance is to zero, the more likely it is that one peer needs to settle with the other over their agreed-upon settlement system.
# Interledger
Building and maintaining your own connector to participate on the Interledger network can be a time consuming and complex undertaking. As a reference implementation of the Interledger stack, Rafiki gives you all the tools you need to join the network and enable Interledger capabilities on your users’ accounts.
## [Packets](#packets)
[Section titled “Packets”](#packets)
At the core of Interledger is the Interledger Protocol (ILP). It’s a request/response protocol where requests and responses are ILP packets.
These packets of data carry information about a payment. Typically, information about a single aggregate payment from sender to receiver is split into multiple ILP packets.
Each packet represents a conditional IOU which affects financial accounting balances between peers. Amounts adjust based on the sender’s asset, the receiver’s asset, and Rafiki’s configured [exchange rates service](/integration/requirements/exchange-rates). Then, the amounts are used to update account liquidity in your accounting database (TigerBeetle or Postgres).
## [Peers](#peers)
[Section titled “Peers”](#peers)
Interledger itself is a network of computers that enables sending payment messages across payment networks. Each computer on the network is a node.
For two nodes on the Interledger network to exchange ILP packets with one another, the two nodes must be peers. There are a number of [requirements](/integration/requirements/peers#perform-prerequisites) that both you and your potential peer must meet to form a peering relationship.
Since the purpose of peering is to facilitate payments, which often involves extending lines of credit, your peer should be someone you trust. We strongly recommend you and your potential peer define your expectations and outline your agreements in a legally binding document peering with one another.
## [Connectors](#connectors)
[Section titled “Connectors”](#connectors)
Each node on the Interledger network can take on the role of sender, connector, or receiver, depending on the payment.
* Sender - Originates the payment by sending ILP packets.
* Connector - An intermediary between a sender and receiver that forwards ILP packets. Connectors can facilitate payments to or from anyone they’re peered with.
* Receiver - The final recipient of the ILP packets and, as such, the payment.
If the sender and receiver nodes are peers, then the payment flow is straightforward and no intermediary connector nodes are needed. However, if the sender and receiver aren’t peers, then the payment must route through one or more connectors. Rafiki’s [backend service](/integration/deployment/services/backend-service#interledger-connector) includes an Interledger connector for sending and receiving ILP packets.
In the image below, the sender node (A) and the receiver node (C) share a common peer (B). In payments from the sender to the receiver, node B performs the role of connector to facilitate payments between the two.

In reality, there can be multiple connectors between a sender node and a receiver node. As more nodes that support different assets peer with one another, the easier it becomes for payments to traverse the Interledger network.
## [Payment pointer](#payment-pointer)
[Section titled “Payment pointer”](#payment-pointer)
A payment pointer is a type of wallet address that serves as an SPSP endpoint to facilitate sending and receiving ILP packets. Rafiki will assign each of your customers’ accounts with a payment pointer.
Payment pointers must resolve to an HTTPS URL and can be written out using the `$` shorthand (for example, `$wallet.example.com/alice`) or as a URL (for example, `https://wallet.example.com/alice`).
See [Payment pointers and wallet addresses](/overview/concepts/payment-pointers) for more information.
## [Simple Payment Setup Protocol (SPSP)](#simple-payment-setup-protocol-spsp)
[Section titled “Simple Payment Setup Protocol (SPSP)”](#simple-payment-setup-protocol-spsp)
The [Simple Payment Setup Protocol (SPSP)](https://interledger.org/developers/rfcs/simple-payment-setup-protocol/) is an application layer protocol that uses HTTPS to exchange payment information. Every payment pointer issued by Rafiki serves as an SPSP endpoint by default (`ENABLE_SPSP_PAYMENT_POINTERS`).
When a `GET` request is made to a payment pointer, the response contains the ILP address of the destination account and a shared secret. These details are needed to set up a STREAM connection between two counterparties to facilitate direct payments over Interledger.
## [STREAM Protocol](#stream-protocol)
[Section titled “STREAM Protocol”](#stream-protocol)
The [STREAM Protocol](https://interledger.org/developers/rfcs/stream-protocol/) is the transport layer protocol in the Interledger Protocol stack. After SPSP communicates the ILP address of the destination account and the shared secret, the STREAM protocol uses these details to set up a STREAM connection between the counterparties.
Rafiki’s `backend` service includes an Interledger connector for sending and receiving STREAM packets. STREAM packets are encoded, encrypted, and sent as the `data` field in ILP packets. The protocol uses the shared secret to authenticate and encrypt packets, and to generate conditions and fulfillments.
A critical function of STREAM is to determine the path exchange rate and handle any changes in the rate. Rafiki’s exchange rates service allows you to provide exchange rate information.
# Multi-tenancy
Multi-tenancy is an architectural approach that enables a single Rafiki instance to service multiple account servicing entities (ASEs). This allows organizations to share application services and database resources while maintaining data isolation and security. By implementing multi-tenancy, Rafiki simplifies the integration process for ASEs, making onboarding faster and easier.
In a multi-tenant environment, the entity responsible for managing a Rafiki instance that serves multiple ASEs is called an **operator**. Each ASE that uses the shared Rafiki instance is called a **tenant**.
## [Operator and tenant roles and responsibilities](#operator-and-tenant-roles-and-responsibilities)
[Section titled “Operator and tenant roles and responsibilities”](#operator-and-tenant-roles-and-responsibilities)
Both operators and tenants have distinct roles and responsibilities.
### [Operator](#operator)
[Section titled “Operator”](#operator)
An operator oversees operational and administrative tasks associated with managing a shared Rafiki instance, including:
* Creating, updating, and deleting tenants
* Managing assets, liquidity, peering relationships, rate information, and other details, including the ability to manage these on behalf of other tenants
* Onboarding new tenants
* Holding Open Payments resources
* Owning the wallet root domain
* Configuring global settings for the Rafiki instance
* Monitoring the health and performance of the Rafiki instance
* Managing the underlying infrastructure of the Rafiki instance
* The operator also has the same capabilities as a tenant
Tenants are added through the Backend Admin API or the [Rafiki Admin application](/admin/admin-user-guide/#tenants). Only operators can create new tenants in a Rafiki instance. When adding new tenants, operators should communicate credentials out-of-band with the tenant.
### [Tenant](#tenant)
[Section titled “Tenant”](#tenant)
A tenant is an ASE that connects to a shared Rafiki instance rather than running its own environment. To connect to the shared environment, each tenant must install and run their own integration service. Tenants are responsible for the following:
* Creating and managing wallet addresses for their users (for example, their customers)
* Sending and receiving payments
* Configuring tenant-specific settings, such as the webhook URL and exchange rates URL
* Managing their own assets and liquidity
## [Benefits of multi-tenancy](#benefits-of-multi-tenancy)
[Section titled “Benefits of multi-tenancy”](#benefits-of-multi-tenancy)
* Centralized maintenance lets operators perform updates once for all tenants.
* Enhanced onboarding allows new ASEs to connect to the shared environment without deploying their own Rafiki instance.
* Simplified administration provides operators with a quick way to add and remove tenants.
### [Key considerations](#key-considerations)
[Section titled “Key considerations”](#key-considerations)
With centralized maintenance, all changes affect all tenants. There’s no way to isolate upgrades or environment maintenance activities. Tenants have limited customization capabilities beyond basic configuration like ILP addresses and identity provider URLs.
# Open Payments
Rafiki follows the [Open Payments](https://openpayments.dev) standard to enable third-party clients to securely retrieve account information and authorize payments from your customers’ accounts with their consent. The standard describes a uniform way to create and manage grants and resources for [incoming payments, quotes, and outgoing payments](https://openpayments.dev/concepts/resources/#resource-types).
## [Rafiki’s backend service](#rafikis-backend-service)
[Section titled “Rafiki’s backend service”](#rafikis-backend-service)
Rafiki’s [`backend`](/integration/deployment/services/backend-service) service is the main service for handling business logic and external communication. The service is responsible for, among other things, exposing the endpoints of the Open Payments APIs for clients to perform account management tasks. Every request and response is validated against the [Open Payments specification](https://github.com/interledger/open-payments/tree/main/openapi).
## [Rafiki’s auth service](#rafikis-auth-service)
[Section titled “Rafiki’s auth service”](#rafikis-auth-service)
Rafiki’s [`auth`](/integration/deployment/services/auth-service) service is a reference implementation of an opinionated Open Payments authorization server. The authorization server is responsible for delegating authorization (via grants) to clients to use the Open Payments APIs, resolving [clients’ public keys](https://openpayments.dev/identity/client-keys/) to authenticate and authorize incoming requests, and creating payments and quotes on the backend. Open Payments leverages the Grant Negotiation and Authorization Protocol (GNAP) for delegating authorization. You can learn more about the protocol by reviewing its [specification](https://datatracker.ietf.org/doc/html/draft-ietf-gnap-core-protocol).
## [Example use cases](#example-use-cases)
[Section titled “Example use cases”](#example-use-cases)
### [Retrieving account information](#retrieving-account-information)
[Section titled “Retrieving account information”](#retrieving-account-information)
Your customer uses a third-party app that allows them to create budgets and monitor their spending. To get the information it needs, the app uses the Open Payments APIs to request your customer’s transaction history.
### [eCommerce payments](#ecommerce-payments)
[Section titled “eCommerce payments”](#ecommerce-payments)
Your customer begins a purchase from an online merchant. Since the merchant has implemented Open Payments, your customer can enter their wallet address into the merchant’s checkout form instead of their credit card details. The merchant’s server uses the Open Payments APIs to communicate with you to set up the payment and get your customer’s consent for the purchase.
### [Peer-to-peer payments (for example, remittances)](#peer-to-peer-payments-for-example-remittances)
[Section titled “Peer-to-peer payments (for example, remittances)”](#peer-to-peer-payments-for-example-remittances)
Your customer uses a third-party remittance app to send money from the US to their family in Mexico. They want their father to receive an exact amount in Mexican pesos, regardless of how much it will cost in USD. Since the app developer has implemented Open Payments, your customer can enter the wallet address for themselves and their father instead of entering bank account details. The app uses the Open Payments APIs to communicate with you to set up the payment and get your customer’s consent for the payment.
### [Further reading](#further-reading)
[Section titled “Further reading”](#further-reading)
We strongly encourage you to familiarize yourself with the Open Payments standard. Extensive documentation is available on the [Open Payments](https://openpayments.dev) website. We recommend you start by reviewing all the pages in the *Intro to Open Payments* section. Here are a few links to get you started.
* [Getting started with Open Payments](https://openpayments.dev/overview/getting-started/)
* [Client keys](https://openpayments.dev/identity/client-keys/)
* [HTTP message signatures](https://openpayments.dev/identity/http-signatures/)
* [Grant negotiation and authorization](https://openpayments.dev/identity/grants/)
# Payment pointers and wallet addresses
## [Payment pointers](#payment-pointers)
[Section titled “Payment pointers”](#payment-pointers)
A payment pointer is a standardized identifier for a payment account that supports Interledger payments. Each payment pointer must resolve to an HTTPS URL that serves as an [SPSP](/overview/concepts/interledger#simple-payment-setup-protocol-spsp) endpoint to facilitate sending and receiving ILP packets.
You can determine whether a URL is a payment pointer by sending a `GET` request to the URL with an `accept: application/spsp4+json` header.
Example request
```http
curl --request GET \
--url https://wallet.example.com/alice/ \
--header 'accept: application/spsp4+json'
```
A response from an SPSP server means the URL is a payment pointer.
Example response
```http
{
"destination_account":"example.0.cloudnine.ind.alice.cdfa5e16-e759",
"shared_secret":"7h0s7EpQDqcgzqmX-mwrNHFHinPvJq8Jw",
}
```
Payment pointers are often written out using the `$` shorthand. For example, `$wallet.example.com/alice`, which resolves to `https://wallet.example.com/alice/`.
Rafiki assigns each of your customers’ accounts with a payment pointer. This payment pointer is also a wallet address because Rafiki supports both Interledger and Open Payments.
## [Wallet addresses](#wallet-addresses)
[Section titled “Wallet addresses”](#wallet-addresses)
A wallet address is a secure, unique URL of a payment account that supports Open Payments. It acts as an entry point into the Open Payments APIs, facilitating interactions like sending and receiving payments.
You can determine whether a URL is a wallet address by sending a `GET` request to the URL with an `accept: application/json` header.
Example request
```http
curl --request GET \
--url https://wallet.example.com/alice \
--header 'accept: application/json'
```
A valid response means the URL is a wallet address.
Example response
```http
{
"id": "https://wallet.example.com/alice",
"publicName": "Alice",
"assetCode": "USD",
"assetScale": 2,
"authServer": "https://auth.wallet.example.com/123e4567-e89b-12d3-a456-426614174000",
"resourceServer": "https://wallet.example.com/123e4567-e89b-12d3-a456-426614174000",
}
```
The `authServer` and `resourceServer` URLs include the tenant ID (UUID v4) as a path segment to ensure that Open Payments requests are routed to the correct tenant.
Rafiki assigns each of your customers’ accounts with a wallet address. This wallet address is also a payment pointer because Rafiki supports Open Payments and Interledger. See the integration requirements for [wallet addresses](/integration/requirements/wallet-addresses) for more information.
# Telemetry
The objective of the telemetry feature is to gather metrics and establish an infrastructure for visualizing valuable network insights. Some metrics that we at the Interledger Foundation collect include:
* The total amount of money transferred via packet data in a specified time (daily, weekly, monthly)
* The number of transactions that have been at least partially successful
* The number of ILP packets flowing through the network
* The average amount of money held in the network per transaction
* The average time it takes for an outgoing payment to complete
Our goals are to:
* Track the growth of the network in terms of transaction sizes and the number of transactions processed
* Use the data for our own insights
* Enable you to gain your own insights
### [Privacy and optionality](#privacy-and-optionality)
[Section titled “Privacy and optionality”](#privacy-and-optionality)
Privacy is a paramount concern for the Interledger Foundation. Rafiki’s telemetry feature is designed to provide valuable network insights without violating privacy or aiding malicious ASEs. Review the [Privacy](#privacy) section below for more information.
The telemetry feature is currently enabled by default on test environments (environments not dealing with real money). When active, the feature transmits metrics to the testnet collector. You can opt in to sharing your metrics with a livenet collector when operating in a production livenet environment (with real money). Regardless of environment, you can also opt-out of telemetry completely. Review the [telemetry environment variables](#telemetry-environment-variables) for more information.
### [Architecture](#architecture)
[Section titled “Architecture”](#architecture)

### [OpenTelemetry (OTEL)](#opentelemetry-otel)
[Section titled “OpenTelemetry (OTEL)”](#opentelemetry-otel)
The Interledger Foundation has adopted [OpenTelemetry (OTEL)](https://opentelemetry.io/) to ensure compliance with a standardized framework that’s compatible with a variety of tool suites. OTEL allows you to use your preferred tools for data analysis, while Rafiki is instrumented and observable through a standardized metrics format.
### [Telemetry Elastic Container Service (ECS) cluster](#telemetry-elastic-container-service-ecs-cluster)
[Section titled “Telemetry Elastic Container Service (ECS) cluster”](#telemetry-elastic-container-service-ecs-cluster)
The Telemetry Replica service is hosted on AWS ECS Fargate and is configured for availability and load balancing of custom ADOT (AWS Distro for OpenTelemetry) Collector ECS tasks.
When you opt for telemetry, metrics are sent to our Telemetry service. To enable you to build your own telemetry solutions, instrumented Rafiki can send data to multiple endpoints. This allows for the integration of a local [OTEL Collector](https://opentelemetry.io/docs/collector/) container that can support custom requirements. Metrics communication is facilitated through [gRPC](https://grpc.io/).
### [OTEL SDK - Rafiki instrumentation](#otel-sdk---rafiki-instrumentation)
[Section titled “OTEL SDK - Rafiki instrumentation”](#otel-sdk---rafiki-instrumentation)
The OTEL SDK is integrated into Rafiki to create, collect, and export metrics. The SDK integrates seamlessly with the OTEL Collector.
### [Prometheus - AMP](#prometheus---amp)
[Section titled “Prometheus - AMP”](#prometheus---amp)
The Interledger Foundation uses Amazon Managed Service for Prometheus (AMP) to collect data from the telemetry cluster.
Note
AMP offers limited configuration options and can’t crawl data outside AWS. This limitation led us to adopt a push model, using `prometheusRemoteWrite`, instead of a pull model. For future development, we may consider hosting our own Prometheus.
### [Grafana - Grafana Cloud](#grafana---grafana-cloud)
[Section titled “Grafana - Grafana Cloud”](#grafana---grafana-cloud)
Grafana Cloud is used for data visualization dashboards and offers multiple tools that extend Prometheus Promql.
Note
The Interledger Foundation initially used Amazon-hosted Grafana which didn’t meet our needs for embedding dashboards. Grafana Cloud offers a feature called *public dashboards* which allows us to share dashboards. However, embedding may still pose a challenge.
### [Exchange rates](#exchange-rates)
[Section titled “Exchange rates”](#exchange-rates)
For telemetry purposes, all amounts collected by instrumented Rafiki should be converted to a base currency.
Privacy reasoning
If only two ASEs are peered over a non-USD currency and we collect data in that currency, it would be easy to determine the volumes moved between those two ASEs. To maintain privacy, we convert all amounts to a base currency.
If an ASE doesn’t provide the necessary exchange rate for a transaction, the telemetry solution still converts the amount to the base currency using external exchange rates. A Lambda function on AWS retrieves and stores the external exchange rates. The function is triggered by a daily `CloudWatch` event and stores the rates in a public S3 bucket. The S3 bucket doesn’t have versioning, and the data is overwritten daily to further ensure privacy.
### [Instrumentation](#instrumentation)
[Section titled “Instrumentation”](#instrumentation)
Rafiki has the following metrics. All data points (counter increases) are exported to collection endpoints at a configurable interval. The default interval is 15 seconds.
| Metric | Type | Description | Behavior |
| ------------------------- | --------- | ------------------------------------------ | ------------------------------------------------------------------------------ |
| `transactions_total` | Counter | Count of funded outgoing transactions | Increases by 1 for each successfully funded outgoing payment resource |
| `packet_count_prepare` | Counter | Count of ILP Prepare packets that are sent | Increases by 1 for each Prepare packet that’s sent |
| `packet_count_fulfill` | Counter | Count of ILP Fulfill packets | Increases by 1 for each Fulfill packet that’s received |
| `packet_count_reject` | Counter | Count of ILP Reject packets | Increases by 1 for each Reject packet that’s received |
| `packet_amount_fulfill` | Counter | Amount sent through the network | Increases by the amount sent in each ILP packet |
| `transaction_fee_amounts` | Counter | Fee amount sent through network | Increases by the amount sent minus the amount received for an outgoing payment |
| `ilp_pay_time_ms` | Histogram | Time to complete an ILP payment | Records the time taken to make an ILP payment |
The current implementation only collects metrics on the SENDING side of a transaction. Metrics for external Open Payments transactions RECEIVED by a Rafiki instance in the network aren’t collected.
## [Privacy](#privacy)
[Section titled “Privacy”](#privacy)
Rafiki telemetry is designed with a strong emphasis on privacy. The system anonymizes user data and refrains from collecting identifiable information. Since transactions can originate from any user to a Rafiki instance, the privacy measures are implemented directly at the source (each Rafiki instance). This means that at the individual level, the data is already anonymous as single Rafiki instances service transactions for multiple users.
### [Differential privacy and local differential privacy (LDP)](#differential-privacy-and-local-differential-privacy-ldp)
[Section titled “Differential privacy and local differential privacy (LDP)”](#differential-privacy-and-local-differential-privacy-ldp)
Differential privacy is a system for publicly sharing information about a dataset by describing the patterns of groups in the dataset while withholding information about individuals in the dataset. Local differential privacy (LDP) is a variant of differential privacy where noise is added to each individual’s data point before the data point is sent to the server. This ensures that the server never sees the actual data, providing a strong privacy guarantee.
### [Rounding technique and bucketing](#rounding-technique-and-bucketing)
[Section titled “Rounding technique and bucketing”](#rounding-technique-and-bucketing)
Rafiki’s telemetry implementation uses a rounding technique that essentially aggregates multiple transactions into the same value, making them indistinguishable. This is achieved by dividing the transaction values into buckets and rounding the values to the nearest bucket.
The bucket size is calculated based on the raw transaction value. For lower value transactions, which are expected to occur more frequently, the bucket sizes are determined linearly for higher granularity. However, after a certain threshold, the bucket size calculation switches to a logarithmic function to ensure privacy for higher value transactions (which are less frequent but pose greater privacy concerns).
To handle outliers, a clipping technique is implemented, capping the buckets. Any value that exceeds a given threshold is placed in a single bucket. Conversely, any value that falls below a certain minimum is also placed in a single bucket. This ensures that both high and low outliers don’t disproportionately affect the overall data, providing further privacy guarantees for these transactions.
### [Laplacian distribution](#laplacian-distribution)
[Section titled “Laplacian distribution”](#laplacian-distribution)
The Laplacian distribution is often used in differential privacy due to its double exponential decay property. This property ensures that a small change in the data doesn’t significantly affect the probability distribution of the output, providing a strong privacy guarantee.
To achieve local differential privacy (LDP), noise is selected from the Laplacian distribution and added to the rounded values. The noise is generated based on a privacy parameter, which is calculated using the sensitivity of the function.
The sensitivity of a function in differential privacy is the maximum amount that any single observation can change the output of the function. In this case, the sensitivity is considered to be the maximum of the rounded value and the bucket size.
The privacy parameter is computed as one-tenth of the sensitivity. This parameter controls the trade-off between privacy and utility: a smaller privacy parameter means more privacy but less utility, and a larger privacy parameter means less privacy but more utility.
The noise, selected from the Laplacian distribution, is then generated using this privacy parameter and added to the rounded value. If the resulting value is zero, the value is set to half the bucket size to ensure that the noise doesn’t completely obscure the transaction value.
### [Currency conversion](#currency-conversion)
[Section titled “Currency conversion”](#currency-conversion)
Another factor that obscures sensitive data is currency conversion. In cross-currency transactions, exchange rates are provided by you, as the ASE, internally. As such, the exchange rates can’t be correlated to an individual transaction. If you don’t or can’t provide the necessary rates, an external API for exchange rates is used. The obtained exchange rates are overwritten frequently in this case, with no versioning or history access. This introduces an additional layer of noise and further protects the privacy of the transactions.
### [Experimental transaction values when using the algorithm](#experimental-transaction-values-when-using-the-algorithm)
[Section titled “Experimental transaction values when using the algorithm”](#experimental-transaction-values-when-using-the-algorithm)
The following table shows the values in the algorithm when running transactions for different amounts. The raw value increases as you move down the rows of the table. All values are in scale 4.
| Raw value | Bucket size | Rounded value | Privacy parameter | Laplace noise | Final value |
| --------- | ----------- | ------------- | ----------------- | ------------- | ----------- |
| 8300 | 10000 | 10000 | 1000 | 2037 | 12037 |
| 13200 | 15000 | 15000 | 1500 | 1397 | 16397 |
| 147700 | 160000 | 160000 | 16000 | -27128 | 132872 |
| 1426100 | 2560000 | 2560000 | 256000 | -381571 | 2178429 |
| 1788200 | 2560000 | 2560000 | 256000 | 463842 | 3023842 |
| 90422400 | 10000000 | 90000000 | 1000000 | 2210649 | 92210649 |
| 112400400 | 10000000 | 100000000 | 1000000 | 407847 | 100407847 |
| 222290500 | 10000000 | 100000000 | 1000000 | -686149 | 99313851 |
### [References](#references)
[Section titled “References”](#references)
Rafiki’s telemetry solution is a combination of techniques described in various white papers on privacy-preserving data collection. More information can be found in the following papers:
* [Local differential privacy for human-centered computing](https://jwcn-eurasipjournals.springeropen.com/articles/10.1186/s13638-020-01675-8)
* [Collecting telemetry data privately](https://www.microsoft.com/en-us/research/blog/collecting-telemetry-data-privately/)
* [RAPPOR: Randomized aggregatable privacy-preserving ordinal response](https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/42852.pdf)
## [Deploy custom telemetry](#deploy-custom-telemetry)
[Section titled “Deploy custom telemetry”](#deploy-custom-telemetry)
Rafiki allows you to build your own telemetry solution based on the [OpenTelemetry (OTEL)](https://opentelemetry.io/) standardized metrics format that Rafiki exposes.
You must deploy your own OTEL Collector that acts as a sidecar container to Rafiki, then provide the OTEL Collector’s ingest endpoint so that Rafiki can begin sending metrics to the collector.
### [Telemetry environment variables](#telemetry-environment-variables)
[Section titled “Telemetry environment variables”](#telemetry-environment-variables)
#### [Required](#required)
[Section titled “Required”](#required)
When the `ENABLE_TELEMETRY` variable is `true`, the following are required.
| Variable name | Type | Description |
| --------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `INSTANCE_NAME` | String | Your Rafiki instance’s name used to communicate for telemetry and autopeering. For telemetry, it’s used to distinguish between the different instances pushing data to the telemetry collector. |
#### [Optional](#optional)
[Section titled “Optional”](#optional)
| Variable name | Type | Description |
| -------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ENABLE_TELEMETRY` | Boolean | Enables the telemetry service on Rafiki. Defaults to `true`. |
| `LIVENET` | Boolean | Determines where to send metrics. Defaults to `false`, resulting in metrics being sent to the testnet OTEL Collector.Set to `true` on production environments dealing with real money. |
| `OPEN_TELEMETRY_COLLECTOR_URLS` | String | A CSV of URLs for OTEL Collectors (for example, `http://otel-collector-NLB-e3172ff9d2f4bc8a.elb.eu-west-2.amazonaws.com:4317,http://happy-life-otel-collector:4317`). |
| `OPEN_TELEMETRY_EXPORT_INTERVAL` | Number | Indicates, in milliseconds, how often the instrumented Rafiki instance should send metrics. Defaults to`15000`. |
| `TELEMETRY_EXCHANGE_RATES_URL` | String | Defines the endpoint Rafiki queries for exchange rates. Used as a fallback if/when [exchange rates](/integration/requirements/exchange-rates) aren’t provided.When set, the response format of the external exchange rates API should be of type `rates`, as is expected by the rate service.Defaults to `https://telemetry-exchange-rates.s3.amazonaws.com/exchange-rates-usd.json`, which points to a public S3 that has the previously mentioned required format, updated daily. |
### [Example Docker OTEL Collector image and configuration](#example-docker-otel-collector-image-and-configuration)
[Section titled “Example Docker OTEL Collector image and configuration”](#example-docker-otel-collector-image-and-configuration)
Below is an example of a Docker OTEL Collector image and configuration that integrates with Rafiki and sends data to a Prometheus remote write endpoint.
You can test the configuration in our [Local Playground](/integration/playground/overview) by providing the environment variables in the preceding table to `happy-life-backend` in the [`docker-compose.yml`](https://github.com/interledger/rafiki/blob/main/localenv/happy-life-bank/docker-compose.yml) file.
#### [Docker Compose config](#docker-compose-config)
[Section titled “Docker Compose config”](#docker-compose-config)
```yaml
# Serves as example for optional local collector configuration
happy-life-otel-collector:
image: otel/opentelemetry-collector-contrib:latest
command: ['--config=/etc/otel-collector-config.yaml', '']
environment:
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID-''}
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY-''}
volumes:
- ../collector/otel-collector-config.yaml:/etc/otel-collector-config.yaml
networks:
- rafiki
expose:
- 4317
ports:
- '13132:13133' # health_check extension
```
#### [OTEL Collector config](#otel-collector-config)
[Section titled “OTEL Collector config”](#otel-collector-config)
Supplemental documentation is available in [OTEL’s Collector Configuration](https://opentelemetry.io/docs/collector/configuration/) documentation.
```yaml
# Serves as example for the configuration of a local OpenTelemetry Collector that sends metrics to an AWS Managed Prometheus Workspace
# Sigv4auth required for AWS Prometheus Remote Write access (USER with access keys needed)
extensions:
sigv4auth:
assume_role:
arn: 'arn:aws:iam::YOUR-ROLE:role/PrometheusRemoteWrite'
sts_region: 'YOUR-REGION'
receivers:
otlp:
protocols:
grpc:
http:
cors:
allowed*origins:
- http://*
- https://\_
processors:
batch:
exporters:
logging:
verbosity: 'normal'
prometheusremotewrite:
endpoint: 'https://aps-workspaces.YOUR-REGION.amazonaws.com/workspaces/ws-YOUR-WORKSPACE-IDENTIFIER/api/v1/remote_write'
auth:
authenticator: sigv4auth
service:
telemetry:
logs:
level: 'debug'
metrics:
level: 'detailed'
address: 0.0.0.0:8888
extensions: [sigv4auth]
pipelines:
metrics:
receivers: [otlp]
processors: [batch]
exporters: [logging, prometheusremotewrite]
```
# Overview
Implementing and maintaining the [Interledger Protocol (ILP)](#interledger) stack on your own can be difficult and time-consuming. Rafiki makes it easy to integrate with the Interledger network without needing to develop and maintain your own implementations.
Rafiki is open-source software maintained by a dedicated team and freely available to any licensed [account servicing entity](/overview/concepts/account-servicing-entity)?A regulated entity that provides and maintains payment accounts for its customers (ASE) wanting to implement Interledger and [Open Payments](#open-payments) on users’ accounts.
Try it out
The [Local Playground](/integration/playground/overview) allows you to test Rafiki by running two mock account servicing entities that automatically peer with one another.
## [Use cases](#use-cases)
[Section titled “Use cases”](#use-cases)
### [Peer-to-peer payments between ASEs](#peer-to-peer-payments-between-ases)
[Section titled “Peer-to-peer payments between ASEs”](#peer-to-peer-payments-between-ases)
In the context of Rafiki, a peer is another ASE with whom you transact. Forming a peering relationship requires you to both agree on the currency in which you will transact, on a settlement mechanism and cadence, and other details. Interledger creates interoperability between different payment systems and currencies, making it easier for peers to directly transact with one another.
### [eCommerce payments](#ecommerce-payments)
[Section titled “eCommerce payments”](#ecommerce-payments)
If a merchant accepts Interledger or Open Payments as a payment method, then a customer can pay using their wallet address instead of entering, for example, a credit card number and other personal details on the merchant’s site. Rafiki’s implementation of Interledger and Open Payments means your account holders can use their wallet addresses for both one-time purchases and recurring purchases, such as subscriptions, anywhere Interledger or Open Payments is an accepted payment method.
### [Web Monetization](#web-monetization)
[Section titled “Web Monetization”](#web-monetization)
With [Web Monetization](https://webmonetization.org), site visitors can pay an amount of their choosing to a participating site with little to no interaction. Both the site and the site visitor must have an Open Payments-enabled wallet address to receive and send payments. Rafiki’s implementation of Interledger’s Simple Payment Setup Protocol (SPSP) and the Open Payments standard means you can assign one or more wallet addresses to your account holders’ accounts, making these accounts support incoming and outgoing Web Monetization payments right out of the box.
## [Interledger](#interledger)
[Section titled “Interledger”](#interledger)
The Interledger network is a network of nodes that have implemented the Interledger Protocol (ILP) stack. Rafiki is a reference implementation if the ILP stack, enabling you to more easily become a node on the network and start sending and receiving payments.
Interledger is designed to be a network on top of existing payment networks that serves as the interoperability layer between them all, forwarding payment messages (packets) while also taking care of currency conversion. Interledger ensures that packets take the fastest and cheapest route from one Interledger node to another.
[Learn more about Interledger](/overview/concepts/interledger)
## [Open Payments](#open-payments)
[Section titled “Open Payments”](#open-payments)
Open Payments is an API standard and set of open RESTful APIs that facilitate interoperability in the setup and completion of payments. The standard provides a uniform way to create and manage grants and resources for incoming payments, quotes, and outgoing payments.
By following the Open Payments standard, Rafiki allows your customers’ accounts to become Open Payments-enabled. Clients, such as mobile apps, can then call the Open Payments APIs to securely retrieve transaction data and initiate payments from your customers’ accounts with your customers’ prior consent.
[Learn more about Open Payments](/overview/concepts/open-payments)
# Architecture
Rafiki is a collection of three services that run together. They can scale horizontally and are designed to support [multi-tenancy](/overview/concepts/multi-tenancy).
* [Backend](/integration/deployment/services/backend-service) - The main service, responsible for handling business logic and external communication
* [Auth](/integration/deployment/services/auth-service) - A reference implementation of an Open Payments authorization server, used for grant authorization and authentication
* [Frontend](/integration/deployment/services/frontend-service) - An optional internal user interface, called the [Rafiki Admin](/admin/admin-user-guide), for you to manage your Rafiki instance
These services rely on a number of databases.
* A Postgres database used by the `auth` service for storing auth-related resources (grants, access tokens, and interactions)
* A Redis database used by the `auth` service to store session data
* A Postgres database used by the `backend` service for Open Payments resources, tenant information, and other application data
* [TigerBeetle](https://github.com/tigerbeetle/tigerbeetle)
, used by the `backend` service for accounting balances
* A Redis database used by the `backend` service as a cache to share STREAM connection details across processes
An additional package for [token introspection](/integration/deployment/services/auth-service#token-introspection) is also included with Rafiki. This is an internal package that requires no action on your part if you’re using Rafiki’s `auth` service.

Note
This diagram illustrates the core Rafiki architecture. In a multi-tenant deployment, multiple tenants would connect to the same Rafiki instance, with the operator managing resources and configurations for each tenant.
# Environment variables
Environment variables are key value pairs used to configure how your Rafiki instance will run in your infrastructure and integrate with your systems. Some environment variables are configured globally for the entire instance, while others can be configured to allow customized settings for each tenant.
Each environment variable name is uppercase, followed by an equal sign and the value of the variable.
Environment variable example
```bash
WEBHOOKS_URL=http://my-business/webhooks
```
The environment variable in the preceding example specifies the HTTP endpoint at which you want your Rafiki instance to send you notifications of webhook events.
To run Rafiki you must set the environment variables for the `backend`, `auth` and `frontend` services where listed as required below.
Caution
If you are running Rafiki via Docker, you must update the variables in the `docker-compose.yml` files of the respective services.
If you are running Rafiki via Helm and Kubernetes, you must update the variables in the [values.yaml](https://github.com/interledger/rafiki/blob/main/infrastructure/helm/rafiki/values.yaml) file of the Helm chart template.
## [Backend](#backend)
[Section titled “Backend”](#backend)
### [Required](#required)
[Section titled “Required”](#required)
| Variable | Helm value name | Default | Description |
| ------------------------------- | ------------------------------------------------------------------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ADMIN_API_SECRET` | *undefined* | *undefined* | Operator API secret used to sign Backend Admin API requests (HMAC SHA-256). Set to a strong, random value. Synced to the operator tenant on startup. |
| `AUTH_SERVICE_API_URL` | *undefined* | *undefined* | The service-to-service API endpoint for propagating tenant information changes to the `auth` server. |
| `AUTH_SERVER_GRANT_URL` | `config.backend.auth.grantUrl` | *undefined* | The endpoint on your Open Payments authorization server to grant a request. |
| `AUTH_SERVER_GRANT_URL` | `config.backend.auth.grantUrl` | *undefined* | The endpoint on your Open Payments authorization server to grant a request. |
| `AUTH_SERVER_INTROSPECTION_URL` | `config.backend.auth.introspectionUrl` | *undefined* | The endpoint on your Open Payments authorization server to introspect an access token. |
| `DATABASE_URL` | `config.backend.databaseUrl.value` or `config.backend.databaseUrl.secretKeyRef` | `postgresql://postgres:password@localhost:5432/development` | The Postgres database URL of the database storing your resource data. Can be provided as a value or secret reference. |
| `EXCHANGE_RATES_URL` | `config.backend.rates.url` | *undefined* | The default exchange rates endpoint. Used if a tenant-specific rates endpoint isn’t configured. |
| `ILP_ADDRESS` | `config.backend.ilp.address` | *undefined* | The ILP address of your Rafiki instance. |
| `ILP_CONNECTOR_URL` | `config.backend.ilp.connector` | *undefined* | The ILP connector address where ILP packets are received. |
| `KEY_ID` | `config.backend.key.id` | *undefined* | Your Rafiki instance’s client key ID. |
| `OPEN_PAYMENTS_URL` | `config.backend.ilp.host` | *undefined* | The public endpoint of your Open Payments resource server. |
| `OPERATOR_TENANT_ID` | *undefined* | *undefined* | The unique identifier of the operator. Must be a UUID v4 generated by the operator. |
| `REDIS_URL` | `config.backend.redisUrl.value` or `config.backend.redisUrl.secretKeyRef` | `redis://127.0.0.1:6379` | The Redis URL of the database handling ILP packet data. Can be provided as a value or secret reference. |
| `USE_TIGERBEETLE` | `config.backend.useTigerbeetle` | `false` | When `true`, a TigerBeetle database is used for accounting. When `false`, a Postgres database is used. |
| `WEBHOOK_URL` | `config.backend.webhook.url` | *undefined* | The default webhook endpoint. Used if a tenant-specific webhook URL isn’t configured. |
### [Conditionally required](#conditionally-required)
[Section titled “Conditionally required”](#conditionally-required)
| Variable | Helm value name | Default | Description |
| ------------------------------------------- | ----------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `DB_ENCRYPTION_SECRET` | *undefined* | *undefined* | When `ENABLE_PARTIAL_PAYMENT_DECISION` is `true`: Base64-encoded secret used to encrypt/decrypt transmitted payment data (`dataToTransmit`) stored on incoming/outgoing payment records and events. |
| `INSTANCE_NAME` | `config.backend.instanceName` | *undefined* | Your Rafiki instance’s name used to communicate for autopeering and/or [telemetry](/overview/concepts/telemetry). Required when autopeering and/or telemetry is enabled |
| `PARTIAL_PAYMENT_DECISION_MAX_WAIT_MS` | `undefined` | `1500` | When `ENABLE_PARTIAL_PAYMENT_DECISION` is `true`, the maximum time, in milliseconds, Rafiki will wait for an ASE to confirm or reject a partial incoming payment. Rafiki rejects the payment if a response isn’t received in time. Only used if this value is less than the `PARTIAL_PAYMENT_DECISION_SAFETY_MARGIN_MS` value. |
| `PARTIAL_PAYMENT_DECISION_SAFETY_MARGIN_MS` | `undefined` | `100` | When `ENABLE_PARTIAL_PAYMENT_DECISION` is `true`, the time, in milliseconds, Rafiki is guaranteed to have to respond before an ILP packet expires. |
| `TRUST_PROXY` | `config.backend.trustProxy` | `false` | Must be set to `true` when running Rafiki behind a proxy. When `true`, the `X-Forwarded-Proto` header is used to determine if connections are secure. |
### [Optional](#optional)
[Section titled “Optional”](#optional)
| Variable | Helm value name | Default | Description |
| ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ADMIN_PORT` | `config.backend.port.admin` | `3001` | The port of your Backend Admin API server. |
| `ADMIN_API_SIGNATURE_TTL_SECONDS` | *undefined* | `30` | The TTL, in seconds, for which a Backend Admin API request signature is valid. |
| `ADMIN_API_SIGNATURE_VERSION` | *undefined* | `1` | The version of the HMAC SHA-256 request-signing algorithm used by the Backend Admin API. |
| `AUTO_PEERING_SERVER_PORT` | `config.backend.port.autoPeering` | `3005` | If autopeering is enabled, the server will use this port. |
| `CONNECTOR_PORT` | `config.backend.port.connector` | `3002` | The port of the ILP connector for sending packets via ILP over HTTP. |
| `ENABLE_AUTO_PEERING` | `config.backend.autoPeering.enabled` | `false` | When `true`, autopeering is enabled. |
| `ENABLE_MANUAL_MIGRATIONS` | *undefined* | `false` | When `true`, you must run the database manually with the command `npm run knex - migrate:latest -env production` |
| `ENABLE_PARTIAL_PAYMENT_DECISION` | *undefined* | `false` | Enables an ASE to act upon (approve/reject) a partial payment. |
| `ENABLE_SPSP_PAYMENT_POINTERS` | *undefined* | `true` | When `true`, the SPSP route is enabled. |
| `ENABLE_TELEMETRY` | `config.backend.telemetry.enabled` | `false` | Enables the telemetry service on Rafiki. |
| `ENABLE_TELEMETRY_TRACES` | *undefined* | `false` | N/A |
| `EXCHANGE_RATES_LIFETIME` | *undefined* | `15000` | The time, in milliseconds, the exchange rates you provide via the `EXCHANGE_RATES_URL` are valid. |
| `GRAPHQL_IDEMPOTENCY_KEY_LOCK_MS` | *undefined* | `2000` | The time to live (TTL), in milliseconds, for `idempotencyKey` concurrency lock on GraphQL mutations on the Backend Admin API. |
| `GRAPHQL_IDEMPOTENCY_KEY_TTL_MS` | *undefined* | `86400000` (24 hours) | The time to live (TTL), in milliseconds, for `idempotencyKey` on GraphQL mutations on the Backend Admin API. |
| `INCOMING_PAYMENT_CREATED_POLL_FREQUENCY_MS` | *undefined* | `1000` | N/A |
| `INCOMING_PAYMENT_CREATED_POLL_TIMEOUT_MS` | *undefined* | `10000` | N/A |
| `INCOMING_PAYMENT_EXPIRY_MAX_MS` | *undefined* | `2592000000` (30 days) | The maximum into the future, in milliseconds, incoming payments expiry can be set to on creation. |
| `INCOMING_PAYMENT_WORKER_IDLE` | *undefined* | `200` | The time, in milliseconds, that `INCOMING_PAYMENT_WORKERS` will wait until checking an empty incoming payment request queue again. |
| `INCOMING_PAYMENT_WORKERS` | *undefined* | `1` | The number of workers processing incoming payment requests. |
| `LOG_LEVEL` | `config.backend.logLevel` | `info` | [Pino log level](https://getpino.io/#/docs/api?id=levels) |
| `LIVENET` | `config.backend.telemetry.livenet` | `false` | When `true`, enables livenet mode for production deployments. |
| `MAX_OUTGOING_PAYMENT_RETRY_ATTEMPTS` | *undefined* | `5` | Specifies how many times an outgoing payment is retried before failing completely. |
| `NODE_ENV` | `config.backend.nodeEnv` | `development` | The type of node environment: `development`, `test`, or `production`. |
| `OPEN_PAYMENTS_PORT` | `config.backend.port.openPayments` | `3000` | The port of your Open Payments resource server. |
| `OPEN_TELEMETRY_COLLECTOR_URLS` | *undefined* | \*undefined | N/A |
| `OPEN_TELEMETRY_EXPORT_INTERVAL` | *undefined* | `15000` | N/A |
| `OPEN_TELEMETRY_TRACE_COLLECTOR_URLS` | *undefined* | *undefined* | N/A |
| `OUTGOING_PAYMENT_WORKER_IDLE` | *undefined* | `10` | The time, in milliseconds, that `OUTGOING_PAYMENT_WORKERS` wait until they check an empty outgoing payment request queue again. |
| `OUTGOING_PAYMENT_WORKERS` | *undefined* | `1` | The number of workers processing outgoing payment requests. |
| `POLL_INCOMING_PAYMENT_CREATED_WEBHOOK` | *undefined* | `false` | N/A |
| `QUOTE_LIFESPAN` | *undefined* | `300000` (5 minutes) | The time, in milliseconds, an Open Payments quote is valid for. |
| `REDIS_TLS_CA_FILE_PATH` | *undefined* | `''` | [Redis TLS config](https://redis.io/docs/latest/operate/oss_and_stack/management/security/encryption/) |
| `REDIS_TLS_CERT_FILE_PATH` | *undefined* | `''` | [Redis TLS config](https://redis.io/docs/latest/operate/oss_and_stack/management/security/encryption/) |
| `REDIS_TLS_KEY_FILE_PATH` | *undefined* | `''` | [Redis TLS config](https://redis.io/docs/latest/operate/oss_and_stack/management/security/encryption/) |
| `SEND_TENANT_WEBHOOKS_TO_OPERATOR` | *undefined* | `false` | When `true`, webhook events for non-operator tenants are also sent to the operator. This allows the operator to monitor and manage events across all tenants. |
| `SIGNATURE_SECRET` | `config.backend.webhookSignatureSecret.value` or `config.backend.webhookSignatureSecret.secretKeyRef` | *undefined* | The secret to generate request header signatures for webhook event requests. |
| `SIGNATURE_VERSION` | *undefined* | `1` | The version number to generate request header signatures for webhook events. |
| `SLIPPAGE` | `config.backend.slippage` | `0.01` (1%) | The accepted ILP rate fluctuation. |
| `TELEMETRY_EXCHANGE_RATES_LIFETIME` | *undefined* | `86400000` (24 hours) | Lifetime for telemetry exchange rates in milliseconds. |
| `TELEMETRY_EXCHANGE_RATES_URL` | *undefined* | `https://telemetry-exchange-rates.s3.amazonaws.com/exchange-rates-usd.json` | The endpoint Rafiki will query for exchange rates. Used as a fallback if/when [exchange rates](/integration/requirements/exchange-rates) aren’t provided. |
| `TIGERBEETLE_CLUSTER_ID` | *undefined* | `0` | The TigerBeetle cluster ID picked by the system that starts the TigerBeetle cluster to create a [TigerBeetle client](https://docs.tigerbeetle.com/clients/node/#creating-a-client). |
| `TIGERBEETLE_REPLICA_ADDRESSES` | *undefined* | `3004` | TigerBeetle replica addresses for all replicas in the cluster. The addresses are comma-separated IP addresses/ports, to create a [TigerBeetle client](https://docs.tigerbeetle.com/clients/node/#creating-a-client). |
| `TIGERBEETLE_TWO_PHASE_TIMEOUT_SECONDS` | *undefined* | `5` | Two-phase timeout for TigerBeetle in seconds. |
| `WALLET_ADDRESS_DEACTIVATION_PAYMENT_GRACE_PERIOD_MS` | *undefined* | `86400000` (24 hours) | The time into the future, in milliseconds, to set expiration of Open Payments incoming payments when deactivating a wallet address. |
| `WALLET_ADDRESS_LOOKUP_TIMEOUT_MS` | *undefined* | `1500` | The time, in milliseconds, you have to create a missing wallet address before timeout. |
| `WALLET_ADDRESS_POLLING_FREQUENCY_MS` | *undefined* | `100` | The frequency of polling while waiting for you to create a missing wallet address. |
| `WALLET_ADDRESS_REDIRECT_HTML_PAGE` | `config.backend.walletAddressRedirectHtmlPage` | *undefined* | Custom HTML page for wallet address redirects. |
| `WALLET_ADDRESS_URL` | `config.backend.ilp.host` | `http://127.0.0.1:3001/.well-known/pay` | Internal base wallet address URL used by the `backend` service. Each tenant’s wallet address base is configured via settings and can’t be updated once set; this variable doesn’t override tenant settings. |
| `WALLET_ADDRESS_WORKER_IDLE` | *undefined* | `200` | The time, in milliseconds, that `WALLET_ADDRESS_WORKERS` wait until checking the empty wallet address request queue again. |
| `WALLET_ADDRESS_WORKERS` | *undefined* | `1` | The number of workers processing wallet address requests. |
| `WEBHOOK_MAX_RETRY` | *undefined* | `10` | The maximum number of times your Rafiki instance’s backend retries sending a certain webhook event to your configured `WEBHOOK_URL`. |
| `WEBHOOK_TIMEOUT` | `config.backend.webhook.timeout` | `2000` (2 seconds) | The time, in milliseconds, that your Rafiki instance will wait for a `200` response from your webhook endpoint. If a `200` response isn’t received, Rafiki will time out and try to send the webhook event again. |
| `WEBHOOK_WORKER_IDLE` | *undefined* | `200` | The time, in milliseconds, that `WEBHOOK_WORKERS` will wait until they check the empty webhook event queue again. |
| `WEBHOOK_WORKERS` | *undefined* | `1` | The number of workers processing webhook events. |
| `WITHDRAWAL_THROTTLE_DELAY` | `config.backend.withdrawalThrottleDelay` | *undefined* | The delay in liquidity withdrawal processing, in milliseconds. |
## [Auth](#auth)
[Section titled “Auth”](#auth)
### [Required](#required)
[Section titled “Required”](#required)
| Variable | Helm value name | Default | Description |
| ------------------------ | --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ADMIN_API_SECRET` | *undefined* | *undefined* | Operator API secret used to sign Auth Admin API requests (HMAC SHA‑256). Set to a strong, random value. Synced to the operator tenant on startup. |
| `AUTH_DATABASE_URL` | `config.auth.databaseUrl.value` or `config.auth.databaseUrl.secretKeyRef` | `postgresql://postgres:password@localhost:5432/auth_development` | The URL of the Postgres database storing your Open Payments grant data. Can be provided as a value or secret reference. |
| `AUTH_SERVER_URL` | `config.auth.authServerUrl` | *undefined* | The public endpoint for your Rafiki instance’s public Open Payments routes. |
| `COOKIE_KEY` | `config.auth.cookieKey.value` or `config.auth.cookieKey.secretKeyRef` | *undefined* | The [koa KeyGrip key](https://koajs.com/#app-keys-) that’s used to sign cookies for an interaction session. |
| `IDENTITY_SERVER_URL` | `config.auth.identityServer.domain` | *undefined* | The URL of your IdP’s server, used by the authorization server to tell an Open Payments client where to redirect the end-user to start interactions. |
| `IDENTITY_SERVER_SECRET` | `config.auth.identityServer.serverSecret.value` or `config.auth.identityServer.serverSecret.secretKeyRef` | *undefined* | A shared secret between the authorization server and the IdP server; the authorization server will use the secret to secure its IdP-related endpoints. When the IdP server sends requests to the authorization server, the IdP server must provide the secret via an [`x-idp-secret`](/integration/requirements/open-payments/idp#x-idp-secret-header) header. |
| `OPERATOR_TENANT_ID` | *undefined* | *undefined* | The unique identifier of the operator. Must be a UUID v4 generated by the operator. |
| `REDIS_URL` | `config.auth.redisUrl.value` or `config.auth.redisUrl.secretKeyRef` | `redis://127.0.0.1:6379` | The connection URL of Redis. Can be provided as a value or secret reference. |
### [Conditionally required](#conditionally-required)
[Section titled “Conditionally required”](#conditionally-required)
| Variable | Helm value name | Default | Description |
| ------------- | ------------------------ | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TRUST_PROXY` | `config.auth.trustProxy` | `false` | Must be set to `true` when running Rafiki behind a proxy. When `true`, the `X-Forwarded-Proto` header is used to determine if connections are secure. |
### [Optional](#optional)
[Section titled “Optional”](#optional)
| Variable | Helm value name | Default | Description |
| --------------------------------- | ----------------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `ACCESS_TOKEN_DELETION_DAYS` | `config.auth.accessToken.deletionDays` | `30` | The days until expired and/or revoked access tokens are deleted. |
| `ACCESS_TOKEN_EXPIRY_SECONDS` | `config.auth.accessToken.expirySeconds` | `600` (10 minutes) | The expiry time, in seconds, for access tokens. |
| `ADMIN_API_SIGNATURE_VERSION` | *undefined* | `1` | The version of the HMAC SHA-256 request-signing algorithm used by the Auth Admin API. |
| `ADMIN_API_SIGNATURE_TTL_SECONDS` | *undefined* | `30` | The TTL, in seconds, for which an Auth Admin API request signature is valid. |
| `ADMIN_PORT` | `config.auth.port.admin` | `3003` | The port of your Rafiki Auth Admin API server. |
| `AUTH_PORT` | `config.auth.port.auth` | `3006` | The port of your Open Payments authorization server. |
| `DATABASE_CLEANUP_WORKERS` | `config.auth.workers.cleanup` | `1` | The number of workers processing expired or revoked access tokens. |
| `ENABLE_MANUAL_MIGRATIONS` | *undefined* | `false` | When `true`, you must run the auth Postgres database manually with the command `npm run knex – migrate:latest –envproduction` |
| `INCOMING_PAYMENT_INTERACTION` | `config.auth.interaction.incomingPayment` | `false` | When `true`, incoming Open Payments grant requests are interactive |
| `INTERACTION_EXPIRY_SECONDS` | *undefined* | `600` (10 minutes) | The time, in seconds, for which a user can interact with a grant request before the request expires. |
| `INTERACTION_PORT` | *undefined* | `3009` | The port number of your Open Payments interaction-related APIs. |
| `INTROSPECTION_PORT` | `config.auth.port.introspection` | `3007` | The port of your Open Payments access token introspection server. |
| `INTERACTION_COOKIE_SAME_SITE` | `config.auth.interaction.cookieSameSite` | *undefined* | The SameSite attribute for interaction cookies. Valid values: `lax`, `none`. |
| `LIST_ALL_ACCESS_INTERACTION` | *undefined* | `true` | When `true`, grant requests that include a `list-all` action will require interaction. In these requests, the client asks to list resources that it didn’t create. |
| `LOG_LEVEL` | `config.auth.logLevel` | `info` | [Pino log level](https://getpino.io/#/docs/api?id=levels) |
| `NODE_ENV` | `config.auth.nodeEnv` | `development` | The type of node environment: `development`, `test`, or `production`. |
| `QUOTE_INTERACTION` | `config.auth.interaction.quote` | `false` | When `true`, quote grants are interactive. |
| `REDIS_TLS_CA_FILE_PATH` | *undefined* | `''` | [Redis TLS config](https://redis.io/docs/latest/operate/oss_and_stack/management/security/encryption/) |
| `REDIS_TLS_CERT_FILE_PATH` | *undefined* | `''` | [Redis TLS config](https://redis.io/docs/latest/oss_and_stack/management/security/encryption/) |
| `REDIS_TLS_KEY_FILE_PATH` | *undefined* | `''` | [Redis TLS config](https://redis.io/docs/latest/operate/oss_and_stack/management/security/encryption/) |
| `SERVICE_API_PORT` | *undefined* | `3011` | The port to expose the internal service API for receiving tenant information changes. |
| `WAIT_SECONDS` | `config.auth.grant.waitSeconds` | `5` | The wait time, in seconds, included in a grant request response (`grant.continue`). |
## [Frontend](#frontend)
[Section titled “Frontend”](#frontend)
### [Required](#required)
[Section titled “Required”](#required)
| Variable | Helm value name | Default | Description |
| ------------------- | ----------------------------------------------- | ----------- | ------------------------------------------ |
| `GRAPHQL_URL` | `config.frontend.serviceUrls.GRAPHQL_URL` | *undefined* | URL of Rafiki’s GraphQL Backend Admin API. |
| `OPEN_PAYMENTS_URL` | `config.frontend.serviceUrls.OPEN_PAYMENTS_URL` | *undefined* | Your Open Payments API endpoint. |
### [Conditionally required](#conditionally-required)
[Section titled “Conditionally required”](#conditionally-required)
The following variables are required only when `AUTH_ENABLED` is set to `true`.
| Variable | Helm value name | Default | Description |
| ----------------------------- | --------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `KRATOS_ADMIN_URL` | *undefined* | *undefined* | The admin endpoint/container address for Kratos. |
| `KRATOS_BROWSER_PUBLIC_URL` | *undefined* | *undefined* | The URL to access the Kratos Docker container from a browser outside the Docker network. This is used for calls from a browser (what you see in the Rafiki Admin UI) to the Kratos server on the backend. |
| `KRATOS_CONTAINER_PUBLIC_URL` | *undefined* | *undefined* | The URL to access the Kratos Docker container from in the Docker network. This is used for backend calls to Kratos. |
### [Optional](#optional)
[Section titled “Optional”](#optional)
| Variable | Helm value name | Default | Description |
| -------------------------------- | -------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AUTH_ENABLED` | `config.frontend.kratos.enabled` | `true` | When `true`, only authenticated users can be granted access to Rafiki Admin by an administrator. |
| `ENABLE_INSECURE_MESSAGE_COOKIE` | *undefined* | *undefined* | When set to `true`, `t`, or `1`, cookie will be transmitted over insecure HTTP connection. Insecure message cookies are required for flash messages to work over HTTP. |
| `LOG_LEVEL` | `config.frontend.logLevel` | `info` | [Pino log level](https://getpino.io/#/docs/api?id=levels). |
| `NODE_ENV` | `config.frontend.nodeEnv` | *undefined* | The type of node environment: `development`, `test`, or `production`. |
| `PORT` | `config.frontend.port` | `3010` | Port from which to host the Rafiki Remix app. |
| `SIGNATURE_VERSION` | *undefined* | *undefined* | The signature version number used when HMAC-signing requests to the Backend Admin API (HMAC SHA-256). |
# Get involved
Welcome to the Rafiki community! Whether you’re a seasoned developer or just getting started with open source, there are many ways to contribute to the Rafiki project. This guide will help you find your place in our vibrant ecosystem.
## [🚀 Get started](#-get-started)
[Section titled “🚀 Get started”](#-get-started)
### [New to Rafiki?](#new-to-rafiki)
[Section titled “New to Rafiki?”](#new-to-rafiki)
* **Explore the project**: Browse the [Rafiki repository](https://github.com/interledger/rafiki) to understand the codebase structure and recent activity.
* **Join our community**:
* Connect with us on [`interledger.slack.com`](https://communityinviter.com/apps/interledger/interledger-working-groups-slack) in the `#rafiki` channel for real-time discussions
* Start or join [GitHub Discussions](https://github.com/interledger/rafiki/discussions) for deeper technical conversations
* **Get hands-on**: Try [running the system locally](/integration/playground/overview) to familiarize yourself with Rafiki’s capabilities. Don’t hesitate to ask questions in the `#rafiki` Slack channel!
### [Make your first contribution](#make-your-first-contribution)
[Section titled “Make your first contribution”](#make-your-first-contribution)
Ready to dive in? Here’s how to review good first issues on GitHub if you are new to open source development.
#### [1: Navigate to the issues tab](#1-navigate-to-the-issues-tab)
[Section titled “1: Navigate to the issues tab”](#1-navigate-to-the-issues-tab)
Go to [good first issues](https://github.com/interledger/rafiki/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) in the Rafiki repo.
#### [2: Check prerequisites](#2-check-prerequisites)
[Section titled “2: Check prerequisites”](#2-check-prerequisites)
Make sure to review our comprehensive [contribution guide](https://github.com/interledger/rafiki/blob/main/.github/contributing.md)
Before claiming an issue, ensure you have:
* Read the issue description and comments
* Reviewed any linked documentation or related issues
* Understood the expected deliverables
* Confirmed the issue hasn’t been resolved in a recent pull request
## [👨💻 Coding](#-coding)
[Section titled “👨💻 Coding”](#-coding)
* [Contribute](https://github.com/interledger/rafiki) to Rafiki coding or testing.
## [📚 Documentation & learning](#-documentation--learning)
[Section titled “📚 Documentation & learning”](#-documentation--learning)
### [Contribute to our documentation](#contribute-to-our-documentation)
[Section titled “Contribute to our documentation”](#contribute-to-our-documentation)
* Improve existing documentation clarity and accuracy
* Create step-by-step tutorials for common use cases
* Develop troubleshooting guides and FAQ sections
* Translate documentation to the following languages:
* Arabic
* Chinese
* French
* German
* Japanese
* Portuguese
* Spanish
* Review translated and localized content
### [Content creation](#content-creation)
[Section titled “Content creation”](#content-creation)
* Write blog posts about your Rafiki integration experiences
* Create video tutorials or demos
* Develop sample applications and code examples
* Share best practices and patterns you’ve discovered
## [🌟 Community building](#-community-building)
[Section titled “🌟 Community building”](#-community-building)
### [Events and engagement](#events-and-engagement)
[Section titled “Events and engagement”](#events-and-engagement)
* Attend the [Interledger Summit](https://interledger.org/summit) and participate in our annual hackathon
* Organize local meetups, coding sprints, and workshops
* Host online webinars or demo sessions
* Mentor new contributors and help them get started
### [Hackathons](#hackathons)
[Section titled “Hackathons”](#hackathons)
* Participate in existing hackathons using Rafiki
* Develop innovative applications and integrations
* Create proof-of-concept projects that showcase Rafiki’s capabilities
* Collaborate with others on experimental features
## [🤝 Getting help](#-getting-help)
[Section titled “🤝 Getting help”](#-getting-help)
Stuck on something? We’re here to help:
* Ask questions in the `#rafiki` Slack channel
* Start a discussion on GitHub for broader topics
* Join our [community calls](https://calendar.google.com/calendar/event?action=TEMPLATE\&tmeid=Z2trYXZsaG1hNWJxZGptbXRzdTlna2p0NXRfMjAyNTA3MDhUMTQzMDAwWiBtb2hhbW1lZEBpbnRlcmxlZGdlci5vcmc\&tmsrc=mohammed%40interledger.org\&scp=ALL)
Remember, every contribution matters - whether it’s a small documentation fix, a bug report, or a major feature implementation. We’re excited to have you as part of the Rafiki community!
# Glossary
## [Account servicing entity (ASE)](#account-servicing-entity-ase)
[Section titled “Account servicing entity (ASE)”](#account-servicing-entity-ase)
An entity that provides and maintains a payment account for a payer and/or payee. An ASE is a regulated entity in the country or countries it operates. Examples include digital wallets, banks, and mobile money providers. Non-regulated entities shouldn’t use Rafiki in production environments due to the potential legal and compliance risks involved.
## [Asset](#asset)
[Section titled “Asset”](#asset)
An asset is made up of a currency `code` and a `scale` that together represent a monetary value. An ISO4217 currency code should be used whenever possible. The `scale` represents the decimal units. For example, US dollars can be delineated as code: USD, with scale 2, where value 1000 represents $10.00.
## [Auth service](#auth-service)
[Section titled “Auth service”](#auth-service)
A reference implementation of an Open Payments authorization server in Rafiki. The `auth` service manages grant authorization and authentication, allowing clients (for example, third-party applications) to create payments and quotes. It issues access tokens and validates client access rights through communication with the resource server.
## [Authorization server](#authorization-server)
[Section titled “Authorization server”](#authorization-server)
A server that grants delegated authorization and privileges, via [GNAP](#grant-negotiation-and-authorization-protocol-gnap), to a particular instance of client software in the form of access tokens, allowing the client to call the Open Payments APIs.
We’ve provided an opinionated version of a GNAP authorization server via the `auth` service. This means that we’ve made certain decisions regarding the implementation and configuration of the server that may limit customization but ensure consistency and adherence to preferred practices.
## [Backend service](#backend-service)
[Section titled “Backend service”](#backend-service)
The core service in Rafiki responsible for managing business logic and external communication. The `backend` service exposes Open Payments API endpoints for account management, operates an Interledger connector for STREAM packet exchange, and provides a GraphQL Backend Admin API for managing accounts and settings.
## [Client](#client)
[Section titled “Client”](#client)
An app or service, such as a mobile or web app, that interacts with the authorization server to obtain grants and access tokens. Clients use tokens to access resource servers and perform actions, such as retrieving transaction history and setting up payments, on behalf of a user or system.
## [Frontend service](#frontend-service)
[Section titled “Frontend service”](#frontend-service)
An optional internal interface in Rafiki, known as the Rafiki Admin, used to manage your Rafiki instance. The `frontend` service communicates with the Backend Admin API through a Remix web app, facilitating administrative tasks in the Rafiki environment.
## [Grant Negotiation and Authorization Protocol (GNAP)](#grant-negotiation-and-authorization-protocol-gnap)
[Section titled “Grant Negotiation and Authorization Protocol (GNAP)”](#grant-negotiation-and-authorization-protocol-gnap)
The Grant Negotiation Authorization Protocol (GNAP) defines a mechanism for delegating authorization to a piece of software (client), and conveying the results and artifacts of that delegation to the software. This delegation can include access to a set of APIs and subject information passed directly to the software. For more information, refer to the [GNAP specification](https://datatracker.ietf.org/doc/html/draft-ietf-gnap-core-protocol-12).
## [Grant](#grant)
[Section titled “Grant”](#grant)
A delegation of authorization from a resource owner to a client, allowing the client to access protected resources or perform actions on the owner’s behalf. In Rafiki, this process is managed by the authorization server, which issues grants as access tokens. These grants permit clients to interact with Open Payments APIs to, for example, create payments and retrieve account information, based on the permissions granted by the resource owner.
## [Identity provider (IdP)](#identity-provider-idp)
[Section titled “Identity provider (IdP)”](#identity-provider-idp)
A system or service that stores and manages user identity information, authentication, and consent. Due to Rafiki’s implementation of the Open Payments standard, Rafiki requires integration with an IdP to support interactive Open Payments grants.
## [Incoming payment](#incoming-payment)
[Section titled “Incoming payment”](#incoming-payment)
An object created by the recipient’s ASE, on their resource server, that represents a payment being received. The object contains information about the incoming payment, such as the amount, currency, receiver’s wallet address, and payment status. The object is used to track and manage payments that are expected to be or have been received.
## [Interledger Protocol (ILP)](#interledger-protocol-ilp)
[Section titled “Interledger Protocol (ILP)”](#interledger-protocol-ilp)
An open protocol stack designed to facilitate the transfer of value across different currencies, platforms, and payment networks. Rafiki is a reference implementation of the Interledger stack, allowing you to join the Interledger network and enable Interledger capabilities on your users’ accounts. For more information, refer to the [Interledger specification](https://interledger.org/developers/get-started/).
## [ILP packet](#ilp-packet)
[Section titled “ILP packet”](#ilp-packet)
A unit of data that carries payment information through the Interledger network. A single payment can be broken into smaller packets of value which are then routed across the network.
## [Open Payments](#open-payments)
[Section titled “Open Payments”](#open-payments)
An API standard and a set of APIs that allows clients to securely retrieve account information and authorize payments from your customers’ accounts with their consent. By adhering to this standard, Rafiki enables integration with external applications and supports the secure and uniform management of payments, quotes, and account data through the Open Payments APIs. For more information, visit the [Open Payments documentation](https://openpayments.dev).
## [Operator](#operator)
[Section titled “Operator”](#operator)
The account servicing entity (ASE) responsible for managing the Rafiki instance and its resources, including tenants, peering relationships, assets, and liquidity. Operators typically have administrative privileges and can perform actions that tenants can’t, such as creating and deleting tenants.
## [Outgoing payment](#outgoing-payment)
[Section titled “Outgoing payment”](#outgoing-payment)
An object created by the sender’s ASE, on their resource server, that represents a payment being sent. This object contains information about the outgoing payment, such as the amount, currency, receiver’s wallet address, and payment status.
## [Payment pointer](#payment-pointer)
[Section titled “Payment pointer”](#payment-pointer)
A type of wallet address that serves as an SPSP endpoint to facilitate sending and receiving ILP packets. Payment pointers can be written out using the `$` shorthand (for example, `$wallet.example.com/alice`) or as a URL (for example, `https://wallet.example.com/alice`).
## [Peer](#peer)
[Section titled “Peer”](#peer)
A counterparty with whom you transact with over the Interledger network. Your Rafiki instance holds liquidity accounts for each of your peers.
## [Quote](#quote)
[Section titled “Quote”](#quote)
An object created by the sender’s ASE, on their resource server, that represents the total cost for the sender to send a payment. When a quote is created, it serves as a commitment from the sender’s ASE to deliver the amount to the recipient’s ASE. Quotes are only valid for a limited time.
## [Resource server](#resource-server)
[Section titled “Resource server”](#resource-server)
A server that hosts and manages access to protected Open Payments resources for incoming payments, quotes, and outgoing payments.
Rafiki’s `backend` service runs an Open Payments resource server.
## [Simple Payment Setup Protocol (SPSP)](#simple-payment-setup-protocol-spsp)
[Section titled “Simple Payment Setup Protocol (SPSP)”](#simple-payment-setup-protocol-spsp)
An Interledger application layer protocol for exchanging payment information between two counterparties to facilitate direct payments over Interledger. The information is then used to set up a STREAM connection. You can read more about SPSP in its [specification](https://interledger.org/developers/rfcs/simple-payment-setup-protocol/).
## [Streaming Transport for the Real-Time Exchange of Assets and Messages (STREAM)](#streaming-transport-for-the-real-time-exchange-of-assets-and-messages-stream)
[Section titled “Streaming Transport for the Real-Time Exchange of Assets and Messages (STREAM)”](#streaming-transport-for-the-real-time-exchange-of-assets-and-messages-stream)
An Interledger transport layer protocol for sending and receiving authenticated ILP packets between peers and determining the path exchange rate. See the [STREAM specification](https://interledger.org/developers/rfcs/stream-protocol/) for more information.
## [Tenant](#tenant)
[Section titled “Tenant”](#tenant)
An account servicing entity (ASE) that uses the shared Rafiki instance to manage its payment accounts and interact with the Interledger network. Tenants have their own isolated set of resources and are managed by an operator. Tenants have limited customization capabilities and can’t manage other tenants.
## [Wallet address](#wallet-address)
[Section titled “Wallet address”](#wallet-address)
A secure, unique URL that identifies an Open Payments-enabled account. It acts as an entry point to the Open Payments APIs, facilitating interactions like sending and receiving payments. Similar to how an email address serves as a public identifier for an email account, a wallet address is publicly shareable and used to interact with the underlying payment account without compromising its security. Wallet address URLs are treated as case-insensitive, meaning that both lowercase and uppercase variations of the same address will be recognized as identical.
## [Web Monetization](#web-monetization)
[Section titled “Web Monetization”](#web-monetization)
A browser API that allows websites to signal their ability to receive Web Monetization payments from their site visitors. Rafiki supports Web Monetization payment flows natively, enabling seamless integration for websites looking to implement the feature and monetize their content via the Interledger network. For more details, visit the [Web Monetization](https://webmonetization.org/) website.
# Releases
## [Release notes](#release-notes)
[Section titled “Release notes”](#release-notes)
Refer to the Rafiki releases [page](https://github.com/interledger/rafiki/releases) for the latest release notes.
## [Rafiki packages](#rafiki-packages)
[Section titled “Rafiki packages”](#rafiki-packages)
The latest sources for each of the Rafiki [services](/resources/architecture) are available through GitHub.
* [Backend](https://github.com/interledger/rafiki/tree/main/packages/backend)
* [Auth](https://github.com/interledger/rafiki/tree/main/packages/auth)
* [Frontend](https://github.com/interledger/rafiki/tree/main/packages/frontend)
Alternatively, if you want to run the latest Rafiki version using Docker Compose, then refer to this [guide](/integration/deployment/docker-compose).
# Webhook event types
Webhooks notify you of specific events that occur in your Rafiki instance, allowing you to integrate Interledger payments with your system and business processes. For example, Rafiki can notify you when one of your account holders has received an Interledger payment, at which point you would credit their account on your ledger.
The following is an enumeration of all Rafiki [webhook event](/integration/requirements/webhook-events) types along with their descriptions, which you must listen to and handle.
| Value | Description |
| ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| [`incoming_payment.created`](/integration/requirements/webhook-events/#incoming-payment-created) | An incoming payment was created. |
| [`incoming_payment.completed`](/integration/requirements/webhook-events/#incoming-payment-completed) | An incoming payment is complete and won’t accept any additional incoming funds. |
| [`incoming_payment.expired`](/integration/requirements/webhook-events/#incoming-payment-expired) | An incoming payment expired and won’t accept any additional incoming funds. |
| [`incoming_payment.partial_payment_received`](/integration/requirements/webhook-events/#incoming-payment-partial-payment-received) | An incoming payment has received a partial payment. |
| [`outgoing_payment.created`](/integration/requirements/webhook-events/#outgoing-payment-created) | An outgoing payment was created. |
| [`outgoing_payment.completed`](/integration/requirements/webhook-events/#outgoing-payment-completed) | An outgoing payment completed. |
| [`outgoing_payment.failed`](/integration/requirements/webhook-events/#outgoing-payment-failed) | An outgoing payment partially or completely failed. |
| [`wallet_address.not_found`](/integration/requirements/webhook-events/#wallet-address-not-found) | A requested wallet address wasn’t found. |
| [`wallet_address.web_monetization`](/integration/requirements/webhook-events/#wallet-address-web-monetization) | Web Monetization payments received via STREAM. |
| [`asset.liquidity_low`](/integration/requirements/webhook-events/#asset-liquidity-low) | Asset liquidity has dropped below defined threshold. |
| [`peer.liquidity_low`](/integration/requirements/webhook-events/#peer-liquidity-low) | Peer liquidity has dropped below defined threshold. |
# Hello from Rafiki
> Rafiki is open source software that provides an efficient solution for an account servicing entity (ASE) to enable Interledger functionality on its users' accounts.
[Try it out!](/integration/playground/overview)
[Test Rafiki by running two mock ASEs that automatically peer with one another.](/integration/playground/overview)
[Integration requirements](/integration/requirements/overview)
[Review the requirements for deploying Rafiki to a production environment.](/integration/requirements/overview)
[View Backend API schema](/apis/graphql/backend)
[Discover what’s in our Backend GraphQL schema.](/apis/graphql/backend)
[View Auth API schema](/apis/graphql/auth)
[Discover what’s in our Auth GraphQL schema.](/apis/graphql/auth)
# 404 - Page Not Found
> It seems like the page you're looking for doesn't exist.
# Rafiki Admin application user guide
The Rafiki Admin application provides tools to manage peers, assets, wallet addresses, webhooks, payments, and account settings. It functions as an interface to the Rafiki [backend service](/v1-beta/integration/deployment/services/backend-service/) and all actions performed, such as fetching data or executing commands, are passed to the Rafiki `backend` service. The purpose of this document is to help you navigate and use the Rafiki Admin application effectively.
## [Getting started](#getting-started)
[Section titled “Getting started”](#getting-started)
### [Prerequisites](#prerequisites)
[Section titled “Prerequisites”](#prerequisites)
* Familiarity with general Rafiki concepts. The [Rafiki overview](/v1-beta/overview/overview) is a great place to start.
* Running the Rafiki `frontend` package. See [Frontend service](/v1-beta/integration/deployment/services/frontend-service) for more information.
## [Identity and user management](#identity-and-user-management)
[Section titled “Identity and user management”](#identity-and-user-management)
Rafiki Admin relies on the [Ory Kratos](https://www.ory.sh/docs/kratos/ory-kratos-intro) identity and user management solution to handle authentication (login) and user management (account creation and password recovery).
Note
Ory Kratos and Rafiki Admin should be hosted on the same top-level domain. Hosting Kratos on a subdomain is generally not recommended by Ory, but if you choose this approach, ensure you follow the guidelines provided in the [Kratos documentation](https://www.ory.sh/docs/kratos/debug/csrf#running-on-separate-subdomains).
### [Login and account management](#login-and-account-management)
[Section titled “Login and account management”](#login-and-account-management)
Access to Rafiki Admin uses an invitation-only system to ensure that only authorized users can register for an account. New users must be invited by an administrator. The registration flow is not public, so users cannot sign up on their own. Instead, administrators create accounts using the `invite-user` script.
#### [Invite a user](#invite-a-user)
[Section titled “Invite a user”](#invite-a-user)
An administrator (someone with backend interface system access) can run the `invite-user` script in one of two ways: from outside the container on the host machine where Docker is running or directly inside the Rafiki Admin Docker container.
Outside container on host machine
```nginx
docker exec -it npm run invite-user -- example@mail.com
```
Inside Rafiki Admin Docker container
```nginx
npm run invite-user -- example@mail.com
```
After running the `invite-user` script, the script generates a recovery link that also serves as an invitation link. This link is output to the terminal, and the administrator can send it to the user. When the user opens the link in their browser, they are automatically logged in and taken to the account settings page where they can set a new password. Afterward, they can log in normally via the Rafiki Admin URL.
[View full image](/img/v1-beta/admin-guide/account-settings.png)
Note
The invitation link is single-use for security purposes. Once accessed, it becomes invalid.
If sending the link through Slack, ensure you format it as code by placing it inside backticks (\`) to prevent Slack from automatically previewing the link, which would invalidate it. For example:
```js
`http://localhost:4433/self-service/recovery?flow=116250ee-07bd-4b5c-a98e-87406192bb4b&token=miv0yZ7DFKKw8RyBBQvWoOsTRa2TVuZm`
```
#### [Generate a recovery link](#generate-a-recovery-link)
[Section titled “Generate a recovery link”](#generate-a-recovery-link)
Rafiki Admin provides an automated account recovery flow which requires an SMTP mail server for sending recovery links to users. Alternatively, an administrator can generate a recovery link using the same `invite-user` script.
#### [Remove a user](#remove-a-user)
[Section titled “Remove a user”](#remove-a-user)
To remove a user, administrators can run the following script in a terminal window:
```nginx
docker exec -it npm run delete-user -- example@mail.com.
```
### [Why Ory Kratos?](#why-ory-kratos)
[Section titled “Why Ory Kratos?”](#why-ory-kratos)
We chose Kratos for its open-source nature, lightweight design, and robust security features. It eliminates the need to manage password hashing, storage, or account recovery flows ourselves, allowing us to focus on what we do best.
Kratos also enhances security with features like built-in breach detection, secure session management, and regular security updates.
Ory Kratos provides frontend components (such as forms and buttons) for identity management flows like login, and account settings. These components are not fixed in design; they are fetched via API calls which allows us to match the identity management components with Rafiki Admin’s overall look and feel.
Disabling authentication
Rafiki Admin provides access to sensitive data like peering relationships and wallet addresses. Authentication is enabled by default to restrict access to authorized users only.
In the [Local Playground](/integration/playground/overview#rafiki-admin), authentication is disabled by default to simplify local development and testing. To disable it in other environments, set the environment variable `AUTH_ENABLED` to `false`. This should be done with **extreme caution** and only in specific scenarios:
* Secure, non-production environments like the Local Playground for local development and testing.
* Internal systems where Rafiki Admin isn’t exposed externally and other access controls (firewalls, local-only access) ensure that the system is secured.
## [Navigation](#navigation)
[Section titled “Navigation”](#navigation)
After logging in, you’ll be greeted by the main landing page with a left-hand navigation menu. This menu provides access to the main functionality needed to manage your Rafiki instance.
[View full image](/img/v1-beta/admin-guide/home.png)
Each of the following menu items leads to a different page of the Rafiki Admin application:
* [Peers](#peers)
* [Assets](#assets)
* [Wallet addresses](#wallet-addresses)
* [Webhooks](#webhooks)
* [Payments](#payments)
* [Account settings](#account-settings)
## [Peers](#peers)
[Section titled “Peers”](#peers)
The Peers page allows you to manage peering relationships in your Rafiki instance, including viewing, creating, editing, and deleting peers.
On this page, all configured peers appear in a table where you can view the peer name, its ILP address, asset details including the asset type and scale, and the outgoing HTTP endpoint.
[View full image](/img/v1-beta/admin-guide/peers.png)
### [Create peer](#create-peer)
[Section titled “Create peer”](#create-peer)
To create a new peer, select **Create Peer** from the main Peers page.
[View full image](/img/v1-beta/admin-guide/create-peer.png)
Fill out the following fields to configure and create your peer:
| Section | Field | Description |
| ------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------- |
| General Information | Name | A public name for the peer to identify it on your system. |
| | Static ILP Address | The peer’s ILP address, obtained from the peer. |
| | Max Packet Amount | The agreed-upon maximum number of packets a payment is split into. |
| HTTP Information | Incoming Auth Tokens | A comma-separated list of tokens accepted by your Rafiki instance from a peer for incoming requests. |
| | Outgoing Auth Token | A single token used by your Rafiki instance for all outgoing requests to authenticate itself with the peer. |
| | Outgoing Endpoint | The URL of the peer’s server where your Rafiki instance sends outgoing requests. |
| Asset Information | Asset | The asset used for transactions with this peer. |
After completing these fields, select **Create** to add the new peer.
### [Edit peer](#edit-peer)
[Section titled “Edit peer”](#edit-peer)
To edit an existing peer, select any peer entry from the table on the main Peers page. This opens the Edit Peer page where you can view and change peer settings.
[View full image](/img/v1-beta/admin-guide/edit-peer.png)
While the Edit Peer page shares fields with the Create Peer page, it also includes fields and actions specific to managing an existing peer:
| Section | Field/Action | Description |
| --------------------- | ------------------ | ----------------------------------------------------------------------------------------- |
| General Information | Peer ID | A unique identifier assigned by Rafiki when the peer was created. This cannot be changed. |
| Asset Information | View Asset | For more information about an asset, select **View asset**. |
| Liquidity Information | Amount | Current amount of peer liquidity available. |
| | Deposit Liquidity | To increase the amount of liquidity available, select **Deposit liquidity**. |
| | Withdraw Liquidity | To reduce the amount of liquidity available, select **Withdraw liquidity**. |
After editing any of the preceding fields in the General Information or HTTP Information sections, select **Save** to commit those changes.
#### [Delete peer](#delete-peer)
[Section titled “Delete peer”](#delete-peer)
The final section of the Peers page is the irreversible action of deleting a peer. Select **Delete peer** to make this change.

Confirm the deletion by typing “delete peer” into the text field and selecting **Delete this peer**.
## [Assets](#assets)
[Section titled “Assets”](#assets)
The Assets page allows you to manage assets in your Rafiki instance, including viewing, editing, and creating assets.
On this page, all configured assets appear in a table where you can view the asset ID, the asset code, the scale, and the withdrawal threshold.
[View full image](/img/v1-beta/admin-guide/assets.png)
### [Create asset](#create-asset)
[Section titled “Create asset”](#create-asset)
To create a new asset, select **Add Asset** from the main Assets page.
[View full image](/img/v1-beta/admin-guide/create-asset.png)
Fill out the following fields to create your new asset:
| Section | Field | Description |
| ------------------- | -------------------- | --------------------------------------------------------------------------------- |
| General Information | Code | The asset code, generally an ISO 4217 currency code where available. |
| | Scale | Difference in order of magnitude between the standard unit and a fractional unit. |
| | Withdrawal Threshold | The minimum amount of liquidity that can be withdrawn from the asset. |
After completing these fields, select **Create** to add the new asset.
### [Edit asset](#edit-asset)
[Section titled “Edit asset”](#edit-asset)
To edit an existing asset, select any asset entry from the table on the main Assets page. This opens the Edit Asset page where you can view and change asset settings.
[View full image](/img/v1-beta/admin-guide/edit-asset.png)
While the Edit Asset page shares fields with the Create Asset page, it also includes fields and actions specific to managing an existing asset:
| Section | Field/Action | Description |
| --------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| General Information | Asset ID | A unique identifier assigned by Rafiki when the asset was created. This cannot be changed. |
| Liquidity Information | Amount | Current amount of asset liquidity available. |
| | Deposit Liquidity | To increase the amount of liquidity available, select **Deposit liquidity**. |
| | Withdraw Liquidity | To reduce the amount of liquidity available, select **Withdraw liquidity**. |
| Sending Fee | Fixed Fee | Flat fee per asset, specified in the smallest unit as defined by the asset scale. In our screenshot above, a fixed fee value of 100 with a scale of 2 represents 1 USD. |
| | Basis Points | A variable fee per asset. One basis point fee is equal to 0.01% of the total amount, 100 basis points = 1%, 10000 basis points = 100% |
| | Fee history | To view a list of asset fees over time, select **Fee history**. |
After editing any of the preceding fields in the General Information or Sending Fee sections, select **Save** to commit those changes.
## [Wallet addresses](#wallet-addresses)
[Section titled “Wallet addresses”](#wallet-addresses)
The Wallet Addresses page allows you to manage the wallet addresses associated with your Rafiki instance, including viewing, editing, and creating wallet addresses.
On this page, all configured wallet addresses appear in the table where you can view the address URL, the public name, and the wallet status.
[View full image](/img/v1-beta/admin-guide/wallet-addresses.png)
### [Create wallet address](#create-wallet-address)
[Section titled “Create wallet address”](#create-wallet-address)
To create a new wallet address, select **Create wallet address** from the main Wallet Address page.
[View full image](/img/v1-beta/admin-guide/create-wallet-address.png)
Fill out the following fields to create a new wallet address:
| Section | Field | Description |
| ------------------- | ------------------- | --------------------------------------------------------------------------- |
| General Information | Wallet address name | The case-insensitive URL of the wallet. Once set, it cannot be changed. |
| | Public name | The name associated with the wallet that is visible to anyone with the URL. |
| | Asset | Select an asset to associate with this wallet. |
Wallet address requirements
* At least one asset must be created before creating a new wallet address. Refer to [Create asset](#create-asset) for more information.
* Wallet address URLs are treated as case-insensitive, meaning that both lowercase and uppercase variations of the same address will be recognized as identical.
After completing this section, select **Create** to add the new wallet address.
### [Edit wallet address](#edit-wallet-address)
[Section titled “Edit wallet address”](#edit-wallet-address)
To edit an existing wallet address, select any wallet address entry from the table on the main Wallet Addresses page. This opens the Edit Wallet Address page where you can view and change wallet address details.
[View full image](/img/v1-beta/admin-guide/edit-wallet-address.png)
While the Edit Wallet Address page shares fields with the Create Wallet Address page, it also includes fields and actions specific to managing an existing wallet address.
| Section | Field/Action | Description |
| --------------------- | -------------------- | ------------------------------------------------------------------------------------------- |
| General Information | ID | A unique identifier assigned by Rafiki when the wallet was created. This cannot be changed. |
| | URL | The wallet address. This cannot be changed. |
| | Status | The current status of the wallet, either active or inactive. |
| Asset Information | Code | The asset code, generally an ISO 4217 currency code where available. |
| | Scale | Difference in order of magnitude between the standard unit and a fractional unit. |
| | Withdrawal threshold | The minimum amount of liquidity that can be withdrawn from the asset. |
| | View asset | For more information about an asset, select **View asset**. |
| Liquidity Information | Amount | Current amount of liquidity available for this wallet. |
| | Withdraw | To withdraw funds from this wallet, select **Withdraw**. |
After editing any of the preceding fields in the General Information section, select **Save** to commit those changes.
What if I need to edit or delete a wallet address?
When managing wallet addresses in Rafiki, there are certain restrictions and limitations to be aware of:
**Editing wallet address URLs**
The URL of an existing wallet address cannot be edited. Changing the URL could disrupt the transaction history associated with that wallet address, potentially leading to inaccuracies in payment records.
**Deleting wallet addresses**
Wallet addresses cannot be deleted from the system. This restriction exists to ensure that any payments tied to a wallet address remain intact and accessible for reporting purposes.
**Solution**
In both cases, the recommended approach is to create a new wallet address and deactivate the old one. To deactivate a wallet address, change the wallet status to Inactive.
## [Webhooks](#webhooks)
[Section titled “Webhooks”](#webhooks)
The Webhook Events page allows you to monitor and manage webhook events within your Rafiki instance. Webhook events in Rafiki are the main communication channel between you and your Rafiki instance. See Webhook events for more information about webhook events.
All triggered webhook events appear in the table. For each webhook event, you can see the webhook ID, the event type, and the date and time of the event. A field at the top of the page allows you to filter the table by event type, making it easier to drill down into specific events.
[View full image](/img/v1-beta/admin-guide/webhooks.png)
To view the webhook event as a JSON representation, select **View data**.
## [Payments](#payments)
[Section titled “Payments”](#payments)
The Payments page allows you to view all incoming and outgoing payments in your Rafiki instance. This allows you to monitor payment activity as well as track the status of payments.
All payments appear in the table. For each payment, you can view the unique payment ID, type of payment, state of the payment, and the date and time the payment was created. A field at the top of the page allows you to filter the table by payment type, making it easier to drill down into specific transactions.
[View full image](/img/v1-beta/admin-guide/payments.png)
## [Account settings](#account-settings)
[Section titled “Account settings”](#account-settings)
The Account Settings page allows you to manage your personal account information, including updating your email address and password.
[View full image](/img/v1-beta/admin-guide/account-settings.png)
To change your email address, enter the new email address and select **Save**.
To change your password, enter your new password and select **Save**.
# Asset liquidity
Asset liquidity is the amount of value, denominated in a given asset, that Rafiki has available to handle cross-currency (foreign exchange) transactions between you and your peer. Whenever an outgoing payment/incoming payment is in a different asset than the peering relationship, the liquidity of asset accounts change depending on the FX direction.
You should deposit and withdraw liquidity as necessary, based on your risk tolerance. Rafiki fails any transaction that would cause an asset’s liquidity to fall below zero.
For more information about how Rafiki handles liquidity, see the [Accounting](/v1-beta/overview/concepts/accounting) concepts page and the [low asset liquidity](/v1-beta/integration/requirements/webhook-events#low-asset-liquidity) section of the webhook events page.
## [Manage asset liquidity using Rafiki Admin](#manage-asset-liquidity-using-rafiki-admin)
[Section titled “Manage asset liquidity using Rafiki Admin”](#manage-asset-liquidity-using-rafiki-admin)
You can deposit and withdraw asset liquidity through the Rafiki Admin application’s [Assets](/v1-beta/admin/admin-user-guide/#edit-asset) screen.
## [Manage asset liquidity using the Backend Admin API](#manage-asset-liquidity-using-the-backend-admin-api)
[Section titled “Manage asset liquidity using the Backend Admin API”](#manage-asset-liquidity-using-the-backend-admin-api)
Idempotency key
You must provide an `idempotencyKey` when calling any mutations related to liquidity. This unique key ensures duplicate or retried requests are processed only once. For more information, see the [Idempotency](/apis/graphql/admin-api-overview#idempotency) section in the GraphQL Admin APIs page.
### [Deposit asset liquidity](#deposit-asset-liquidity)
[Section titled “Deposit asset liquidity”](#deposit-asset-liquidity)
* Operation
```graphql
mutation DepositAssetLiquidity($input: DepositAssetLiquidityInput!) {
depositAssetLiquidity(input: $input) {
assetId
amount
id
idempotencyKey
success
}
}
```
* Variables
```json
{
"input": {
"assetId": "7b8b0f65-896d-4403-b7ba-2e24bf20eb35",
"amount": "100",
"id": "b97fd85a-126e-42ef-b40d-1a50a70ffa6f",
"idempotencyKey": "b97fd85a-126e-42ef-b40d-1a50a70ffa6f",
}
}
```
For more information about this mutation’s input object, see [`DepositAssetLiquidityInput`](https://rafiki.dev/apis/graphql/backend/inputobjects/#depositassetliquidityinput).
* Response
When an asset liquidity deposit is successful, `DepositAssetLiquidity` returns `true`.
```json
{
data: {
success: true
}
}
```
### [Withdraw asset liquidity](#withdraw-asset-liquidity)
[Section titled “Withdraw asset liquidity”](#withdraw-asset-liquidity)
* Operation
```graphql
mutation CreateAssetLiquidityWithdrawal($input: CreateAssetLiquidityWithdrawalInput!) {
createAssetLiquidityWithdrawal(input: $input) {
success
}
}
```
* Variables
```json
{
"input": {
"id": "b97fd85a-126e-42ef-b40d-1a50a70ffa6f",
"assetId": "7b8b0f65-896d-4403-b7ba-2e24bf20eb35",
"amount": "100",
"idempotencyKey": "b97fd85a-126e-42ef-b40d-1a50a70ffa6f",
"timeoutSeconds": 0
}
}
```
For more information about this mutation’s input object, see [`CreateAssetLiquidityWithdrawalInput`](https://rafiki.dev/apis/graphql/backend/inputobjects/#createassetliquiditywithdrawalinput).
* Response
When an asset liquidity withdrawal is successful, `CreateAssetLiquidityWithdrawal` returns `true`.
```json
{
data: {
success: true
}
}
```
# Payment liquidity
Payment liquidity represents:
* The value received from a completed incoming payment
* The value available to send in an outgoing payment
Because Rafiki doesn’t hold funds, anything you receive in an incoming payment must be withdrawn and then credited to the recipient’s account on your ledger. Listen for the [incoming payments](/v1-beta/integration/requirements/webhook-events#incoming-payments) webhook events to know when you need to interact with Rafiki.
Any excess liquidity that remains after an outgoing payment completes must be withdrawn. You may also find that you must deposit liquidity into Rafiki to fund an outgoing payment. Listen for Rafiki’s [outgoing payments](/v1-beta/integration/requirements/webhook-events#outgoing-payments) webhook events to know when action is required on your part.
Rafiki Admin
The Rafiki Admin doesn’t allow you to manage payment liquidity, but you can view details about incoming and outgoing payments through the app’s [Payments](/v1-beta/admin/admin-user-guide#payments) screen.
For more information about how Rafiki handles liquidity, see the [Accounting](/v1-beta/overview/concepts/accounting) concepts page.
## [Manage payment liquidity using the Backend Admin API](#manage-payment-liquidity-using-the-backend-admin-api)
[Section titled “Manage payment liquidity using the Backend Admin API”](#manage-payment-liquidity-using-the-backend-admin-api)
Idempotency key
You must provide an `idempotencyKey` when calling any mutations related to liquidity. This unique key ensures duplicate or retried requests are processed only once. For more information, see the [Idempotency](/apis/graphql/admin-api-overview#idempotency) section in the GraphQL Admin APIs page.
### [Withdraw incoming payment liquidity](#withdraw-incoming-payment-liquidity)
[Section titled “Withdraw incoming payment liquidity”](#withdraw-incoming-payment-liquidity)
* Operation
```graphql
mutation CreateIncomingPaymentWithdrawal($input: CreateIncomingPaymentWithdrawalInput!) {
createIncomingPaymentWithdrawal(input: $input) {
success
}
}
```
* Variables
```json
{
"input": {
"incomingPaymentId": "b4f85d5c-652d-472d-873c-4ba2a5e39052",
"idempotencyKey": "a09b730d-8610-4fda-98fa-ec7acb19c775",
"timeoutSeconds": 0
}
}
```
For more information about this mutation’s input object, see [`CreateIncomingPaymentWithdrawalInput`](https://rafiki.dev/apis/graphql/backend/inputobjects/#createincomingpaymentwithdrawalinput).
* Response
When an incoming payment liquidity withdrawal is successful, `CreateIncomingPaymentWithdrawal` returns `true`.
```json
{
data: {
success: true
}
}
```
### [Deposit outgoing payment liquidity](#deposit-outgoing-payment-liquidity)
[Section titled “Deposit outgoing payment liquidity”](#deposit-outgoing-payment-liquidity)
* Operation
```graphql
mutation DepositOutgoingPaymentLiquidity($input: DepositOutgoingPaymentLiquidityInput!) {
depositOutgoingPaymentLiquidity(input: $input) {
success
}
}
```
* Variables
```json
{
"input": {
"outgoingPaymentId": "b4f85d5c-652d-472d-873c-4ba2a5e39052",
"idempotencyKey": "a09b730d-8610-4fda-98fa-ec7acb19c775"
}
}
```
For more information about this mutation’s input object, see [`DepositOutgoingPaymentLiquidityInput`](https://rafiki.dev/apis/graphql/backend/inputobjects/#depositoutgoingpaymentliquidityinput).
* Response
When an outgoing payment liquidity deposit is successful, `DepositOutgoingPaymentLiquidity` returns `true`.
```json
{
data: {
success: true
}
}
```
### [Withdraw outgoing payment liquidity](#withdraw-outgoing-payment-liquidity)
[Section titled “Withdraw outgoing payment liquidity”](#withdraw-outgoing-payment-liquidity)
* Operation
```graphql
mutation CreateOutgoingPaymentWithdrawal($input: CreateOutgoingPaymentWithdrawalInput!) {
createOutgoingPaymentWithdrawal(input: $input) {
success
}
}
```
* Variables
```json
{
"input": {
"outgoingPaymentId": "b4f85d5c-652d-472d-873c-4ba2a5e39052",
"idempotencyKey": "a09b730d-8610-4fda-98fa-ec7acb19c775",
"timeoutSeconds": 0
}
}
```
For more information about this mutation’s input object, see [`CreateOutgoingPaymentWithdrawalInput`](https://rafiki.dev/apis/graphql/backend/inputobjects/#createoutgoingpaymentwithdrawalinput).
* Response
When an outgoing payment liquidity withdrawal is successful, `CreateOutgoingPaymentWithdrawal` returns `true`.
```json
{
data: {
success: true
}
}
```
# Peer liquidity
Peer liquidity is the line of credit you extend to a peer, denominated in your agreed upon asset. A peer’s liquidity account balance represents the amount of credit the peer still has available to them.
A peer’s liquidity increases when payments are made to the peer and decreases when payments are made from the peer. For example, if a customer of your peer sends your customer a payment of $20 USD, then your peer’s liquidity account decreases by 20.
If a peer’s liquidity is insufficient (for example, they’ve used up their allotted credit line), transactions initiated from the peer will fail. Once a peer’s liquidity is used up, you should settle with your peer and reset their liquidity. Deposit and withdraw peer liquidity as necessary, based on your risk profile.
For more information about how Rafiki handles liquidity, see the [Accounting](/v1-beta/overview/concepts/accounting) concepts page and the [low peer liquidity](/v1-beta/integration/requirements/webhook-events#low-peer-liquidity) section of the webhook events page.
## [Manage peer liquidity using Rafiki Admin](#manage-peer-liquidity-using-rafiki-admin)
[Section titled “Manage peer liquidity using Rafiki Admin”](#manage-peer-liquidity-using-rafiki-admin)
You can deposit and withdraw peer liquidity through the Rafiki Admin app’s [Peers](/v1-beta/admin/admin-user-guide/#edit-peer) screen.
## [Manage peer liquidity using the Backend Admin API](#manage-peer-liquidity-using-the-backend-admin-api)
[Section titled “Manage peer liquidity using the Backend Admin API”](#manage-peer-liquidity-using-the-backend-admin-api)
Idempotency key
You must provide an `idempotencyKey` when calling any mutations related to liquidity. This unique key ensures duplicate or retried requests are processed only once. For more information, see the [Idempotency](/apis/graphql/admin-api-overview#idempotency) section in the GraphQL Admin APIs page.
### [Deposit peer liquidity](#deposit-peer-liquidity)
[Section titled “Deposit peer liquidity”](#deposit-peer-liquidity)
* Operation
```graphql
mutation DepositPeerLiquidity($input: DepositPeerLiquidityInput!) {
depositPeerLiquidity(input: $input) {
success
}
}
```
* Variables
```json
{
"input": {
"id": "a09b730d-8610-4fda-98fa-ec7acb19c775",
"peerId": "73158598-2e0c-4973-895e-aebd115af260",
"amount": "1000000",
"idempotencyKey": "a09b730d-8610-4fda-98fa-ec7acb19c775"
}
}
```
For more information about this mutation’s input object, see [`DepositPeerLiquidityInput`](https://rafiki.dev/apis/graphql/backend/inputobjects/#depositpeerliquidityinput).
* Response
When a peer liquidity deposit is successful, `DepositPeerLiquidity` returns `true`.
```json
{
"data": {
"success": true
}
}
```
### [Withdraw peer liquidity](#withdraw-peer-liquidity)
[Section titled “Withdraw peer liquidity”](#withdraw-peer-liquidity)
* Operation
```graphql
mutation CreatePeerLiquidityWithdrawal($input: CreatePeerLiquidityWithdrawalInput!) {
createPeerLiquidityWithdrawal(input: $input) {
success
}
}
```
* Variables
```json
{
"input": {
"id": "421fae87-9a59-4217-9ff8-faf55ffab9c6",
"peerId": "73158598-2e0c-4973-895e-aebd115af260",
"amount": "100",
"idempotencyKey": "b97fd85a-126e-42ef-b40d-1a50a70ffa6f",
"timeoutSeconds": 0
}
}
```
For more information about this mutation’s input object, see [`CreatePeerLiquidityWithdrawalInput`](https://rafiki.dev/apis/graphql/backend/inputobjects/#createpeerliquiditywithdrawalinput).
* Response
When a peer liquidity withdrawal is successful, `CreatePeerLiquidityWithdrawal` returns `true`.
```json
{
"data": {
"success": true
}
}
```
# Two-phase transfers
Rafiki allows for two-phase transfers, which moves funds in two stages.
1. Reserve funds (`pending`)
2. Resolve funds (`post`, `void`, or `expire`)
The following transactions support two-phase transfers:
* Asset liquidity withdrawal
* Peer liquidity withdrawal
* Incoming payment withdrawal
* Outgoing payment withdrawal
* Wallet address withdrawal
When a withdraw-liquidity transaction is requested with a timeout greater than `0`, the transaction processes as a two-phase transfer. A `0` denotes the absence of a timeout.
If the timeout interval passes before the transaction posts or is voided, the transaction expires and the full amount is returned to the original account.
## [Manage two-phase transfers using the Backend Admin API](#manage-two-phase-transfers-using-the-backend-admin-api)
[Section titled “Manage two-phase transfers using the Backend Admin API”](#manage-two-phase-transfers-using-the-backend-admin-api)
Idempotency key
You must provide an `idempotencyKey` when calling any mutations related to liquidity. This unique key ensures duplicate or retried requests are processed only once. For more information, see the [Idempotency](/apis/graphql/admin-api-overview#idempotency) section in the GraphQL Admin APIs page.
### [Post and commit a successful transfer](#post-and-commit-a-successful-transfer)
[Section titled “Post and commit a successful transfer”](#post-and-commit-a-successful-transfer)
* Operation
```graphql
mutation PostLiquidityWithdrawal($input: PostLiquidityWithdrawalInput!) {
postLiquidityWithdrawal(input: $input) {
error
success
}
}
```
* Variables
```json
{
"input": {
"withdrawalId": "b4f85d5c-652d-472d-873c-4ba2a5e39052",
"idempotencyKey": "a09b730d-8610-4fda-98fa-ec7acb19c775"
}
}
```
For more information about this mutation’s input object, see [`PostLiquidityWithdrawalInput`](https://rafiki.dev/apis/graphql/backend/inputobjects/#postliquiditywithdrawalinput).
* Response
When a liquidity withdrawal post is successful, `PostLiquidityWithdrawal` returns `true`.
```json
{
data: {
success: true
}
}
```
### [Void and roll-back an unsuccessful transfer](#void-and-roll-back-an-unsuccessful-transfer)
[Section titled “Void and roll-back an unsuccessful transfer”](#void-and-roll-back-an-unsuccessful-transfer)
* Operation
```graphql
mutation VoidLiquidityWithdrawal($input: VoidLiquidityWithdrawalInput!) {
voidLiquidityWithdrawal(input: $input) {
error
success
}
}
```
* Variables
```json
{
"input": {
"withdrawalId": "b4f85d5c-652d-472d-873c-4ba2a5e39052",
"idempotencyKey": "a09b730d-8610-4fda-98fa-ec7acb19c775"
}
}
```
For more information about this mutation’s input object, see [`VoidLiquidityWithdrawalInput`](https://rafiki.dev/apis/graphql/backend/inputobjects/#voidliquiditywithdrawalinput).
* Response
When a liquidity withdrawal is successfully voided and rolled back, `VoidLiquidityWithdrawal` returns `true`.
```json
{
data: {
success: true
}
}
```
# Overview
Rafiki provides two GraphQL APIs, described below. As described on [GraphQL.org](https://graphql.org/), GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL APIs are organized in terms of types and fields, not endpoints.
## [Backend Admin API](#backend-admin-api)
[Section titled “Backend Admin API”](#backend-admin-api)
The Backend Admin API provides you with comprehensive capabilities to manage your Rafiki instance. Core functionality includes managing peering relationships, assets, wallet addresses and their public keys, as well as liquidity management through deposits and withdrawals. Another important aspect of the Backend Admin API is to manage Open Payments resources like payments and quotes.
This API is complemented by the Rafiki Admin application, a frontend interface for Rafiki management that directly interacts with the Backend Admin API. Check out the [Rafiki Admin Application User Guide](/v1-beta/admin/admin-user-guide) for more information.
## [Auth Admin API](#auth-admin-api)
[Section titled “Auth Admin API”](#auth-admin-api)
The Auth Admin API allows you to get information about a grant, such as its status, state, related payment details, and the wallet address of the grantee’s account. The API also allows you to revoke grants.
## [Idempotency](#idempotency)
[Section titled “Idempotency”](#idempotency)
Wikipedia describes [idempotence](https://en.wikipedia.org/wiki/Idempotence) as being the property of “certain operations in…computer science whereby \[the operations] can be applied multiple times without changing the result beyond the initial application.” “An operation can be repeated or retried as often as necessary without causing unintended effects. With non-idempotent operations, the algorithm may have to keep track of whether the operation was already performed.”
Several mutations in the Admin APIs utilize an idempotency key to allow for safely retrying requests without performing operations multiple times. The key should be unique, typically a V4 UUID.
For the Admin APIs, whenever a mutation with an `idempotencyKey` is called, the request payload and the request response are saved under that key. Any subsequent requests made with the same idempotency key will return the original response and status of the request, regardless of whether the request was successful. Keys are cached for a default of 24 hours. The default can be changed via the `backend` service’s `GRAPHQL_IDEMPOTENCY_KEY_TTL_MS backend` environment flag.
Additionally, in the chance that a request is made while still concurrently processing the first request under the same `idempotencyKey`, the APIs will return an error. This provides further safeguards from potential errors in the system. The timing to prevent processing concurrent requests is `2` seconds by default. The default can be changed via the `backend` service’s `GRAPHQL_IDEMPOTENCY_KEY_LOCK_MS` environment flag.
# Docker Compose
This guide is an example of deploying Rafiki using Docker Compose with Nginx as a reverse proxy on a virtual machine (VM) in a cloud environment. This guide also uses [Certbot](https://certbot.eff.org/) to generate [Let’s Encrypt TLS certificates](https://letsencrypt.org/) to secure exposed ports using HTTPS.
From [Docker’s documentation](https://docs.docker.com/compose/), Docker Compose is a tool for defining and running multi-container applications using a single YAML file. It simplifies the process of configuring and running multiple services.
Note
While Docker Compose could serve as a production deployment, it is an environment best suited for development and testing. For more information, refer to Docker’s [documentation](https://docs.docker.com/compose/intro/features-uses/#common-use-cases-of-docker-compose) on common use cases.
## [Prerequisites](#prerequisites)
[Section titled “Prerequisites”](#prerequisites)
### [Deploy VM and install Docker](#deploy-vm-and-install-docker)
[Section titled “Deploy VM and install Docker”](#deploy-vm-and-install-docker)
Recommended software version
We recommended using the latest vendor supported version for each of the software dependencies listed in this section.
Deploy a general purpose VM with the following minimum specifications:
* OS: Linux distro
* RAM: 4 GB
* vCPUs: 2
Install the following software on the VM:
* [Docker Engine](https://docs.docker.com/engine/install/)
* [Docker Compose](https://docs.docker.com/compose/install/#scenario-two-install-the-compose-plugin)
### [Install Nginx and Certbot](#install-nginx-and-certbot)
[Section titled “Install Nginx and Certbot”](#install-nginx-and-certbot)
Once you have provisioned the VM in your cloud environment, install Nginx along with Certbot:
```sh
sudo apt update && sudo apt install nginx certbot python3-certbot-nginx
```
### [Domain preparation](#domain-preparation)
[Section titled “Domain preparation”](#domain-preparation)
Generate the Let’s Encrypt certificates using Certbot:
```sh
certbot certonly --manual --preferred-challenges=dns --email EMAIL --server https://acme-v02.api.letsencrypt.org/directory --agree-tos -d DOMAIN
```
Update TXT record
Domain can be in wildcard format. You will also need to update the TXT record in this step.
As Let’s Encrypt certificates are valid for 90 days, you must set up a cron process to renew the certificate on a regular schedule:
```sh
crontab -e
0 3 * * * certbot renew
```
### [Domain and DNS configuration](#domain-and-dns-configuration)
[Section titled “Domain and DNS configuration”](#domain-and-dns-configuration)
Map the [Open Payments resource server](/v1-beta/integration/deployment/services/backend-service#open-payments) to your domain, and the [ILP connector](/v1-beta/integration/deployment/services/backend-service#interledger-connector), [Open Payments auth server](/v1-beta/integration/deployment/services/auth-service), and [Admin UI](/v1-beta/integration/deployment/services/frontend-service) to subdomains. Using the DNS host of your choice, set up your domain and subdomains according to the following recommended convention:
| Service | Exposes | URL | Example |
| ----------------------------- | ----------------------------------------------------------------- | ------------ | ------------------ |
| Open Payments resource server | Open Payments APIs | DOMAIN | myrafiki.com |
| ILP connector | ILP connector to send and receive ILP packets between peers | ilp.DOMAIN | ilp.myrafiki.com |
| Open Payments auth server | Reference implementation of an Open Payments authorization server | auth.DOMAIN | auth.myrafiki.com |
| Admin UI | Admin UI to manage Rafiki | admin.DOMAIN | admin.myrafiki.com |
Note
The example domain and subdomain values are for demonstration purposes only. You must use the actual domain names that you set up with your DNS host.
Next, update the DNS records (A records) to point to the static external IP address of the virtual machine according to the table above.
## [Configure Compose file](#configure-compose-file)
[Section titled “Configure Compose file”](#configure-compose-file)
The Docker Compose file is a YAML configuration file used to define the services, networks, and volumes that make up a multi-container application. In this section, we’ll explore the Compose file by breaking it down into the individual Rafiki services.
Update Compose file
Before using the Compose file, you must update the variables with values relevant to your environment. Specifically, change the values enclosed within the brackets and substitute `newest-version` with the latest Rafiki [version](https://github.com/interledger/rafiki/releases).
### [Docker Compose example](#docker-compose-example)
[Section titled “Docker Compose example”](#docker-compose-example)
While the actual Compose file is a single YAML file containing all of the services, this page will guide you through each service one by one. For each service, we’ll look at the relevant configuration details along with the corresponding environment variables.
#### [Auth service](#auth-service)
[Section titled “Auth service”](#auth-service)
The Rafiki `auth` service is responsible for handling authentication and authorization for your application. It connects to a Postgres database to store auth-related resources and a Redis database for storing session data. See [Auth service](/v1-beta/integration/deployment/services/auth-service/) for more information.
Ports exposed:
* 3003 (`ADMIN_PORT`) is used for the Auth Admin API
* 3006 (`AUTH_PORT`) is used for the Open Payments authorization server
Make sure to configure the `AUTH_DATABASE_URL` and `REDIS_URL` environment variables to point to your database instances.
Running Rafiki behind a proxy
If you plan to run your Rafiki instance behind a proxy, you must set the `TRUST_PROXY` variable to `true`
```sh
rafiki-auth:
image: ghcr.io/interledger/rafiki-auth:
container_name: rafiki-auth
environment:
AUTH_DATABASE_URL: {postgresql://postgres:password@localhost:5432/auth_development}
AUTH_SERVER_URL: {https://auth.myrafiki.com}
ADMIN_PORT: 3003
AUTH_PORT: 3006
INTROSPECTION_PORT: 3007
INTERACTION_PORT: 3009
COOKIE_KEY: {...}
IDENTITY_SERVER_SECRET: {...}
IDENTITY_SERVER_URL: {https://idp.mysystem.com}
REDIS_URL: {redis://127.0.0.1:6379}
TRUST_PROXY: true
depends_on:
- postgres
networks:
- rafiki
ports:
- '3003:3003'
- '3006:3006'
- '3007:3007'
- '3009:3009'
restart: always
```
##### [Environment variables](#environment-variables)
[Section titled “Environment variables”](#environment-variables)
| Variable | Required | Description |
| --------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AUTH_DATABASE_URL` | Y | The URL of the Postgres database storing your Open Payments grant data. |
| `AUTH_SERVER_URL` | Y | The public endpoint for your Rafiki instance’s public Open Payments routes. |
| `COOKIE_KEY` | Y | The [koa KeyGrip key](https://koajs.com/#app-keys-) that is used to sign cookies for an interaction session. |
| `IDENTITY_SERVER_SECRET` | Y | A shared secret between the authorization server and the IdP server; the authorization server will use the secret to secure its IdP-related endpoints. When the IdP server sends requests to the authorization server, the IdP server must provide the secret via an [`x-idp-secret`](/integration/requirements/open-payments/idp#x-idp-secret-header) header. |
| `IDENTITY_SERVER_URL` | Y | The URL of your IdP’s server, used by the authorization server to inform an Open Payments client of where to redirect the end-user to start interactions. |
| `REDIS_URL` | Y | The connection URL for Redis. |
| `ACCESS_TOKEN_DELETION_DAYS` | N | The days until expired and/or revoked access tokens are deleted. |
| `ACCESS_TOKEN_EXPIRY_SECONDS` | N | The expiry time, in seconds, for access tokens. |
| `ADMIN_API_SIGNATURE_TTL_SECONDS` | N | The TTL, in seconds, for which a request’s signature will be valid. |
| `ADMIN_API_SIGNATURE_VERSION` | N | The version of the request signing algorithm used to generate signatures. |
| `ADMIN_PORT` | N | The port of your Rafiki Auth Admin API server. |
| `AUTH_PORT` | N | The port of your Open Payments authorization server. |
| `INCOMING_PAYMENT_INTERACTION` | N | When `true`, incoming Open Payments grant requests are interactive. |
| `INCOMING_PAYMENT_WORKERS` | N | The number of workers processing incoming payment requests. |
| `INTERACTION_EXPIRY_SECONDS` | N | The time, in seconds, for which a user can interact with a grant request before the request expires. |
| `INTERACTION_PORT` | N | The port number of your Open Payments interaction-related APIs. |
| `INTROSPECTION_PORT` | N | The port of your Open Payments access token introspection server. |
| `LIST_ALL_ACCESS_INTERACTION` | N | When `true`, grant requests that include a `list-all` action will require interaction. In these requests, the client asks to list resources that it did not create. |
| `LOG_LEVEL` | N | [Pino log level](https://getpino.io/#/docs/api?id=levels). |
| `NODE_ENV` | N | The type of node environment: `development`, `test`, or `production`. |
| `QUOTE_INTERACTION` | N | When `true`, quote grants are interactive. |
| `REDIS_TLS_CA_FILE_PATH` | N | [Redis TLS config](https://redis.io/docs/latest/operate/oss_and_stack/management/security/encryption/). |
| `REDIS_TLS_CERT_FILE_PATH` | N | [Redis TLS config](https://redis.io/docs/latest/operate/oss_and_stack/management/security/encryption/). |
| `REDIS_TLS_KEY_FILE_PATH` | N | [Redis TLS config](https://redis.io/docs/latest/operate/oss_and_stack/management/security/encryption/). |
| `TRUST_PROXY` | N | Must be set to `true` when running Rafiki behind a proxy. When `true`, the `X-Forwarded-Proto` header is used to determine if connections are secure. |
| `WAIT_SECONDS` | N | The wait time, in seconds, included in a grant request response (`grant.continue`). |
#### [Backend service](#backend-service)
[Section titled “Backend service”](#backend-service)
The Rafiki `backend` service handles business logic and external communication. It exposes the Open Payments APIs and an Interledger connector for sending and receiving packets. It connects to a Redis database for caching, a Postgres database for Open Payments resources, and TigerBeetle for accounting liquidity. See [Backend service](/v1-beta/integration/deployment/services/backend-service) for more information.
TigerBeetle or Postgres for accounting database
TigerBeetle is recommended, but if you would rather use Postgres as an accounting database make sure to set `USE_TIGERBEETLE` to false.
Ports exposed:
* 3000 (`OPEN_PAYMENTS_PORT`) is used for the Open Payments resource server
* 3001 (`ADMIN_PORT`) is used for the Backend Admin API
* 3002 (`CONNECTOR_PORT`) is used for the ILP connector to send and receive ILP packets
Make sure to configure the `DATABASE_URL` and `REDIS_URL` environment variables to point to your database instances.
```sh
rafiki-backend:
image: ghcr.io/interledger/rafiki-backend:
container_name: rafiki-backend
depends_on:
- postgres
- redis
environment:
AUTH_SERVER_GRANT_URL: {https://auth.myrafiki.com}
AUTH_SERVER_INTROSPECTION_URL: {https://auth.myrafiki.com/3007}
DATABASE_URL: {postgresql://postgres:password@localhost:5432/development}
ILP_ADDRESS: {test.myrafiki}
ADMIN_PORT: 3001
CONNECTOR_PORT: 3002
OPEN_PAYMENTS_PORT: 3000
OPEN_PAYMENTS_URL: {https://myrafiki.com}
REDIS_URL: {redis://127.0.0.1:6379}
WALLET_ADDRESS_URL: {https://myrafiki.com/rafiki-instance}
WEBHOOK_URL: {https://mysystem.com/webhooks}
EXCHANGE_RATES_URL: {https://mysystem.com/rates}
ILP_CONNECTOR_URL: {https://ilp.myrafiki.com}
INSTANCE_NAME: {'My ASE name'}
TRUST_PROXY: true
KEY_ID: ...
USE_TIGERBEETLE: true
TIGERBEETLE_CLUSTER_ID: 0
TIGERBEETLE_REPLICA_ADDRESSES: 10.5.0.50:4342
networks:
- rafiki
ports:
- '3000:3000'
- '3001:3001'
- '3002:3002'
privileged: true
restart: always
volumes:
- ../temp/:/workspace/temp/
```
##### [Environment variables](#environment-variables-1)
[Section titled “Environment variables”](#environment-variables-1)
| Variable | Required | Description |
| ----------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AUTH_SERVER_GRANT_URL` | Y | The endpoint on your Open Payments authorization server to grant a request. |
| `AUTH_SERVER_INTROSPECTION_URL` | Y | The endpoint on your Open Payments authorization server to introspect an access token. |
| `DATABASE_URL` | Y | The Postgres database URL of the database storing your resource data. |
| `EXCHANGE_RATES_URL` | Y | The endpoint your Rafiki instance uses to request exchange rates. |
| `ILP_ADDRESS` | Y | The ILP address of your Rafiki instance. |
| `ILP_CONNECTOR_URL` | Y | The ILP connector address where ILP packets are received. |
| `KEY_ID` | Y | Your Rafiki instance’s client key ID. |
| `OPEN_PAYMENTS_URL` | Y | The public endpoint of your Open Payments resource server. |
| `REDIS_URL` | Y | The Redis URL of the database handling ILP packet data. |
| `USE_TIGERBEETLE` | Y | When `true`, a TigerBeetle database is used for accounting. When `false`, a Postgres database is used. |
| `WEBHOOK_URL` | Y | Your endpoint that consumes webhook events. |
| `ADMIN_PORT` | N | The port of your Backend Auth API server. |
| `API_SECRET` | N | N/A |
| `API_SIGNATURE_VERSION` | N | The version of the request signing algorithm used to generate signatures. |
| `AUTO_PEERING_SERVER_PORT` | N | If auto-peering is enabled, the server will use this port. |
| `CONNECTOR_PORT` | N | The port of the ILP connector for sending packets via ILP over HTTP. |
| `ENABLE_AUTO_PEERING` | N | When `true`, auto-peering is enabled. |
| `ENABLE_MANUAL_MIGRATIONS` | N | When `true`, you must run the database manually with the command `npm run knex – migrate:latest –env production` |
| `ENABLE_SPSP_PAYMENT_POINTERS` | N | When `true`, the SPSP route is enabled. |
| `ENABLE_TELEMETRY` | N | Enables the telemetry service on Rafiki. |
| `ENABLE_TELEMETRY_TRACES` | N | N/A |
| `EXCHANGE_RATES_LIFETIME` | N | The time, in milliseconds, the exchange rates you provide via the `EXCHANGE_RATES_URL` are valid. |
| `GRAPHQL_IDEMPOTENCY_KEY_LOCK_MS` | N | The TTL, in milliseconds, for `idempotencyKey` concurrency lock on GraphQL mutations on the Backend Admin API. |
| `GRAPHQL_IDEMPOTENCY_KEY_TTL_MS` | N | The TTL, in milliseconds, for `idempotencyKey` on GraphQL mutations on the Backend Admin API. |
| `INCOMING_PAYMENT_CREATED_POLL_FREQUENCY_MS` | N | N/A |
| `INCOMING_PAYMENT_CREATED_POLL_TIMEOUT_MS` | N | N/A |
| `INCOMING_PAYMENT_EXPIRY_MAX_MS` | N | The maximum into the future, in milliseconds, incoming payments expiry can be set to on creation. |
| `INCOMING_PAYMENT_WORKER_IDLE` | N | The time, in milliseconds, that `INCOMING_PAYMENT_WORKERS` will wait until checking an empty incoming payment request queue again. |
| `INCOMING_PAYMENT_WORKERS` | N | The number of workers processing incoming payment requests. |
| `INSTANCE_NAME` | N | Your Rafiki instance’s name used to communicate for auto-peering and/or [telemetry](/overview/concepts/telemetry). Required when auto-peering and/or telemetry is enabled. |
| `LOG_LEVEL` | N | [Pino log level](https://getpino.io/#/docs/api?id=levels) |
| `MAX_OUTGOING_PAYMENT_RETRY_ATTEMPTS` | N | Specifies how many times an outgoing payment is retried before failing completely. |
| `NODE_ENVIRONMENT` | N | The type of node environment: `development`, `test`, or `production`. |
| `OPEN_PAYMENTS_PORT` | N | The port of your Open Payments resource server. |
| `OPEN_TELEMETRY_COLLECTOR_URLS` | N | N/A |
| `OPEN_TELEMETRY_EXPORT_INTERVAL` | N | N/A |
| `OPEN_TELEMETRY_TRACE_COLLECTOR_URLS` | N | N/A |
| `OUTGOING_PAYMENT_WORKER_IDLE` | N | The time, in milliseconds, that `OUTGOING_PAYMENT_WORKERS` wait until they check an empty outgoing payment request queue again. |
| `OUTGOING_PAYMENT_WORKERS` | N | The number of workers processing outgoing payment requests. |
| `POLL_INCOMING_PAYMENT_CREATED_WEBHOOK` | N | N/A |
| `PRIVATE_KEY_FILE` | N | The path to your Rafiki instance’s client private key. |
| `QUOTE_LIFESPAN` | N | The time, in milliseconds, an Open Payments quote is valid for. |
| `REDIS_TLS_CA_FILE_PATH` | N | [Redis TLS config](https://redis.io/docs/latest/operate/oss_and_stack/management/security/encryption/) |
| `REDIS_TLS_CERT_FILE_PATH` | N | [Redis TLS config](https://redis.io/docs/latest/operate/oss_and_stack/management/security/encryption/) |
| `REDIS_TLS_KEY_FILE_PATH` | N | [Redis TLS config](https://redis.io/docs/latest/operate/oss_and_stack/management/security/encryption/) |
| `SIGNATURE_SECRET` | N | The secret to generate request header signatures for webhook event requests. |
| `SIGNATURE_VERSION` | N | The version number to generate request header signatures for webhook events. |
| `SLIPPAGE` | N | The accepted ILP rate fluctuation. |
| `STREAM_SECRET` | N | The seed secret to generate shared STREAM secrets. |
| `TELEMETRY_EXCHANGE_RATES_LIFETIME` | N | N/A |
| `TELEMETRY_EXCHANGE_RATES_URL` | N | The endpoint Rafiki will query for exchange rates. Used as a fallback if/when [exchange rates](/integration/requirements/exchange-rates) aren’t provided. |
| `TIGERBEETLE_CLUSTER_ID` | N | The TigerBeetle cluster ID picked by the system that starts the TigerBeetle cluster to create a [TigerBeetle client](https://docs.tigerbeetle.com/clients/node/#creating-a-client). |
| `TIGERBEETLE_REPLICA_ADDRESSES` | N | TigerBeetle replica addresses for all replicas in the cluster. The addresses are comma-separated IP addresses/ports, to create a [TigerBeetle client](https://docs.tigerbeetle.com/clients/node/#creating-a-client). |
| `TIGERBEETLE_REPLICA_ADDRESSES.SPLIT` | N | N/A |
| `TIGERBEETLE_TWO_PHASE_TIMEOUT_SECONDS` | N | N/A |
| `TRUST_PROXY` | N | Must be set to `true` when running Rafiki behind a proxy. When `true`, the `X-Forwarded-Proto` header is used to determine if connections are secure. |
| `WALLET_ADDRESS_DEACTIVATION_PAYMENT_GRACE_PERIOD_MS` | N | The time into the future, in milliseconds, to set expiration of Open Payments incoming payments when deactivating a wallet address. |
| `WALLET_ADDRESS_LOOKUP_TIMEOUT_MS` | N | The time, in milliseconds, you have to create a missing wallet address before timeout. |
| `WALLET_ADDRESS_POLLING_FREQUENCY_MS` | N | The frequency of polling while waiting for you to create a missing wallet address. |
| `WALLET_ADDRESS_URL` | N | Your Rafiki instance’s internal wallet address. |
| `WALLET_ADDRESS_WORKER_IDLE` | N | The time, in milliseconds, that `WALLET_ADDRESS_WORKERS` wait until checking the empty wallet address request queue again. |
| `WALLET_ADDRESS_WORKERS` | N | The number of workers processing wallet address requests. |
| `WEBHOOK_MAX_RETRY` | N | The maximum number of times your Rafiki instance’s backend retries sending a certain webhook event to your configured `WEBHOOK_URL`. |
| `WEBHOOK_TIMEOUT` | N | The time, in milliseconds, that your Rafiki instance will wait for a `200` response from your webhook endpoint. If a `200` response is not received, Rafiki will time out and try to send the webhook event again. |
| `WEBHOOK_WORKER_IDLE` | N | The time, in milliseconds, that `WEBHOOK_WORKERS` will wait until they check the empty webhook event queue again. |
| `WEBHOOK_WORKERS` | N | The number of workers processing webhook events. |
| `WITHDRAWAL_THROTTLE_DELAY` | N | The delay in liquidity withdrawal processing. |
#### [Frontend service](#frontend-service)
[Section titled “Frontend service”](#frontend-service)
The Rafiki `frontend` service provides an internal admin interface for managing your Rafiki instance. It communicates with the Backend Admin API to facilitate administrative tasks. See [Frontend service](/v1-beta/integration/deployment/services/frontend-service) for more information.
Ports exposed:
* 3005 (`PORT`) is used to host the Rafiki Admin app
Make sure to configure the `GRAPHQL_URL` and `OPEN_PAYMENTS_URL` environment variables to point to the appropriate endpoints.
```sh
rafiki-frontend:
image: ghcr.io/interledger/rafiki-frontend:
container_name: rafiki-frontend
depends_on:
- rafiki-backend
environment:
PORT: 3005
GRAPHQL_URL: {https://myrafiki.com:3001}
OPEN_PAYMENTS_URL: {https://myrafiki.com}
KRATOS_CONTAINER_PUBLIC_URL: {http://kratos:4433}
KRATOS_BROWSER_PUBLIC_URL: {https://admin.myrafiki.com/kratos}
KRATOS_ADMIN_URL: {http://kratos:4434/admin}
networks:
- rafiki
restart: always
privileged: true
ports:
- '3005:3005'
```
##### [Environment variables](#environment-variables-2)
[Section titled “Environment variables”](#environment-variables-2)
| Variable | Required | Description |
| -------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GRAPHQL_URL` | Y | URL for Rafiki’s GraphQL Auth Admin API. |
| `OPEN_PAYMENTS_URL` | Y | Your Open Payments API endpoint. |
| `PORT` | Y | Port from which to host the Rafiki Remix app. |
| `AUTH_ENABLED` | N | When `true`, only authenticated users can be granted access to Rafiki Admin by an administrator. |
| `ENABLE_INSECURE_MESSAGE_COOKIE` | N | When set to `true`, `t`, or `1`, cookie will be transmitted over insecure HTTP connection. Insecure message cookies are required for flash messages to work over HTTP. |
| `KRATOS_ADMIN_URL` | N | The admin endpoint/container address for Kratos. |
| `KRATOS_BROWSER_PUBLIC_URL` | N | The URL for you to access the Kratos Docker container from a browser outside of the Docker network. This is used for calls from a browser (what you see in the Rafiki Admin UI) to the Kratos server on the backend. |
| `KRATOS_CONTAINER_PUBLIC_URL` | N | The URL for you to access the Kratos Docker container from within the Docker network. This is used for backend calls to Kratos. |
| `LOG_LEVEL` | N | [Pino log level](https://getpino.io/#/docs/api?id=levels). |
| `NODE_ENV` | N | The type of node environment: `development`, `test`, or `production`. |
| `SIGNATURE_SECRET` | N | The signature secret used to authenticate requests to the Backend Admin API. |
| `SIGNATURE_VERSION` | N | The signature version number used to authenticate requests to the Backend Admin API. |
#### [TigerBeetle](#tigerbeetle)
[Section titled “TigerBeetle”](#tigerbeetle)
TigerBeetle is a high-performance database designed to handle double-entry/double-ledger accounting. It is recommended for managing liquidity and settlement accounts due to its speed and efficiency. See [Accounting](/v1-beta/overview/concepts/accounting/#tigerbeetle) for more information.
To use TigerBeetle, make sure that `USE_TIGERBEETLE` is set to true in the backend service environment variables.
```sh
tigerbeetle:
image: ghcr.io/tigerbeetle/tigerbeetle:0.16.29
privileged: true
volumes:
- tigerbeetle-data:/var/lib/tigerbeetle
networks:
rafiki:
ipv4_address: 10.5.0.50
entrypoint:
- /bin/sh
- -c
- |
set -ex
DATA_FILE=/var/lib/tigerbeetle/cluster_0_replica_0.tigerbeetle
set +e
ls $$DATA_FILE
DATA_FILE_EXISTS="$$?"
set -e
echo $$DATA_FILE_EXISTS
if [ "$$DATA_FILE_EXISTS" != 0 ]; then
./tigerbeetle format --cluster=0 --replica=0 --replica-count=1 $$DATA_FILE;
fi
hostname -i
ls /var/lib/tigerbeetle
./tigerbeetle start --addresses=0.0.0.0:4342 $$DATA_FILE
```
#### [Postgres](#postgres)
[Section titled “Postgres”](#postgres)
The Postgres service is a relational database management system used to store and manage application data. Both the `auth` and `backend` services rely on Postgres databases.
```sh
postgres:
image: 'postgres:16'
container_name: postgres
environment:
POSTGRES_USER: ...
POSTGRES_PASSWORD: ...
networks:
- rafiki
restart: unless-stopped
volumes:
- pg-data:/var/lib/postgresql/data
- ../dbinit.sql:/docker-entrypoint-initdb.d/init.sql
```
#### [Redis](#redis)
[Section titled “Redis”](#redis)
The Redis service is used for caching and session management across the application. Both the `auth` and `backend` services rely on Redis databases.
```sh
redis:
image: 'redis:7'
restart: unless-stopped
networks:
- rafiki
```
#### [Kratos](#kratos)
[Section titled “Kratos”](#kratos)
The Kratos service is an identity and user management solution used by Rafiki’s `frontend` service for handling authentication and user management tasks.
Disabling authentication
Rafiki Admin provides access to sensitive data like peering relationships and wallet addresses. Authentication is enabled by default to restrict access to authorized users only.
In the [Local Playground](/integration/playground/overview#rafiki-admin), authentication is disabled by default to simplify local development and testing. To disable it in other environments, set the environment variable `AUTH_ENABLED` to `false`. This should be done with **extreme caution** and only in specific scenarios:
* Secure, non-production environments like the Local Playground for local development and testing.
* Internal systems where Rafiki Admin isn’t exposed externally and other access controls (firewalls, local-only access) ensure that the system is secured.
```sh
kratos:
image: 'oryd/kratos:v1.2.0'
privileged: true
ports:
- '4433:4433'
volumes:
- ../entrypoint.sh:/entrypoint.sh
- ../identity.schema.json:/etc/config/kratos/identity.schema.json
- ./kratos.yml:/etc/config/kratos/kratos.yml
entrypoint: ['/entrypoint.sh']
networks:
- rafiki
```
#### [Networks and volumes](#networks-and-volumes)
[Section titled “Networks and volumes”](#networks-and-volumes)
In Docker Compose, networks and volumes are necessary for enabling communication between services and persisting data storage for containers.
```sh
networks:
testnet:
driver: bridge
ipam:
config:
- subnet: 10.5.0.0/24
gateway: 10.5.0.1
volumes:
pg-data:
tigerbeetle-data:
```
#### [Complete Docker Compose example](#complete-docker-compose-example)
[Section titled “Complete Docker Compose example”](#complete-docker-compose-example)
Click to expand
```sh
name: 'my-rafiki'
services:
rafiki-auth:
image: ghcr.io/interledger/rafiki-auth:
container_name: rafiki-auth
environment:
AUTH_DATABASE_URL: {postgresql://...}
AUTH_SERVER_URL: {https://auth.myrafiki.com}
ADMIN_PORT: 3003
AUTH_PORT: 3006
INTROSPECTION_PORT: 3007
INTERACTION_PORT: 3009
SERVICE_API_PORT: 3011
COOKIE_KEY: {...}
IDENTITY_SERVER_SECRET: {...}
IDENTITY_SERVER_URL: {https://idp.mysystem.com}
REDIS_URL: {redis://...}
TRUST_PROXY: true
depends_on:
- postgres
networks:
- rafiki
ports:
- '3003:3003'
- '3006:3006'
- '3007:3007'
- '3009:3009'
- '3011:3011'
restart: always
rafiki-backend:
image: ghcr.io/interledger/rafiki-backend:
container_name: rafiki-backend
depends_on:
- postgres
- redis
environment:
AUTH_SERVER_GRANT_URL: {https://auth.myrafiki.com}
AUTH_SERVER_INTROSPECTION_URL: {https://auth.myrafiki.com/3007}
AUTH_SERVICE_API_URL: {https://auth.myrafiki.com/3011}
DATABASE_URL: {postgresql://...}
ILP_ADDRESS: {test.myrafiki}
ADMIN_PORT: 3001
CONNECTOR_PORT: 3002
OPEN_PAYMENTS_PORT: 3000
OPEN_PAYMENTS_URL: {https://myrafiki.com}
REDIS_URL: {redis://...}
WALLET_ADDRESS_URL: {https://myrafiki.com/rafiki-instance}
WEBHOOK_URL: {https://mysystem.com/webhooks}
EXCHANGE_RATES_URL: {https://mysystem.com/rates}
ILP_CONNECTOR_URL: {https://ilp.myrafiki.com}
INSTANCE_NAME: {'My ASE name'}
TRUST_PROXY: true
KEY_ID: ...
USE_TIGERBEETLE: true
TIGERBEETLE_CLUSTER_ID: 0
TIGERBEETLE_REPLICA_ADDRESSES: 10.5.0.50:4342
networks:
- rafiki
ports:
- '3000:3000'
- '3001:3001'
- '3002:3002'
privileged: true
restart: always
volumes:
- ../temp/:/workspace/temp/
rafiki-frontend:
image: ghcr.io/interledger/rafiki-frontend:
container_name: rafiki-frontend
depends_on:
- rafiki-backend
environment:
PORT: 3005
GRAPHQL_URL: {https://myrafiki.com:3001}
OPEN_PAYMENTS_URL: {https://myrafiki.com}
KRATOS_CONTAINER_PUBLIC_URL: {http://kratos:4433}
KRATOS_BROWSER_PUBLIC_URL: {https://admin.myrafiki.com/kratos}
KRATOS_ADMIN_URL: {http://kratos:4434/admin}
networks:
- rafiki
restart: always
privileged: true
ports:
- '3005:3005'
tigerbeetle:
image: ghcr.io/tigerbeetle/tigerbeetle:0.16.29
privileged: true
volumes:
- tigerbeetle-data:/var/lib/tigerbeetle
networks:
rafiki:
ipv4_address: 10.5.0.50
entrypoint:
- /bin/sh
- -c
- |
set -ex
DATA_FILE=/var/lib/tigerbeetle/cluster_0_replica_0.tigerbeetle
set +e
ls $$DATA_FILE
DATA_FILE_EXISTS="$$?"
set -e
echo $$DATA_FILE_EXISTS
if [ "$$DATA_FILE_EXISTS" != 0 ]; then
./tigerbeetle format --cluster=0 --replica=0 --replica-count=1 $$DATA_FILE;
fi
hostname -i
ls /var/lib/tigerbeetle
./tigerbeetle start --addresses=0.0.0.0:4342 --development $$DATA_FILE
postgres:
image: 'postgres:16'
container_name: postgres
environment:
POSTGRES_USER: ...
POSTGRES_PASSWORD: ...
networks:
- rafiki
restart: unless-stopped
volumes:
- pg-data:/var/lib/postgresql/data
- ../dbinit.sql:/docker-entrypoint-initdb.d/init.sql
redis:
image: 'redis:7'
restart: unless-stopped
networks:
- rafiki
kratos:
image: 'oryd/kratos:v1.2.0'
privileged: true
ports:
- '4433:4433'
volumes:
- ../entrypoint.sh:/entrypoint.sh
- ../identity.schema.json:/etc/config/kratos/identity.schema.json
- ./kratos.yml:/etc/config/kratos/kratos.yml
entrypoint: ['/entrypoint.sh']
networks:
- rafiki
networks:
rafiki:
driver: bridge
ipam:
config: - subnet: 10.5.0.0/24
gateway: 10.5.0.1
volumes:
pg-data:
tigerbeetle-data:
```
## [Create Nginx config files](#create-nginx-config-files)
[Section titled “Create Nginx config files”](#create-nginx-config-files)
Create Nginx configuration files for every exposed domain:
| Service | URL | Example | Nginx config file |
| ----------------------------- | ------------ | ------------------ | ------------------------------------------------------------------ |
| Open Payments resource server | DOMAIN | myrafiki.com | /etc/nginx/sites-available/open\_payments\_resource\_server.config |
| ILP connector | ilp.DOMAIN | ilp.myrafiki.com | /etc/nginx/sites-available/ilp.config |
| Open Payments auth server | auth.DOMAIN | auth.myrafiki.com | /etc/nginx/sites-available/open\_payments\_auth\_server.config |
| Admin UI | admin.DOMAIN | admin.myrafiki.com | /etc/nginx/sites-available/admin.config |
Note
The example domain and subdomain values are for demonstration purposes only. You must use the actual domain names that you set up with your DNS host.
### [Open Payments resource server (`backend` package)](#open-payments-resource-server-backend-package)
[Section titled “Open Payments resource server (backend package)”](#open-payments-resource-server-backend-package)
Using the editor of your choice, save the following file as `open_payments_resource_server.config` in the `/etc/nginx/sites-available` directory on your VM:
```sh
server {
server_name myrafiki.com;
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/myrafiki.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/myrafiki.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
proxy_http_version 1.1;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade "";
proxy_set_header Connection "";
proxy_set_header Host $server_name;
proxy_set_header Accept-Encoding "";
proxy_cache_bypass $http_upgrade;
proxy_pass_request_headers on;
proxy_pass http://localhost:3000;
}
}
server {
server_name myrafiki.com;
listen 80;
if ($host = myrafiki.com) {
return 301 https://$host$request_uri;
}
return 404;
}
```
### [ILP connector (`backend` package)](#ilp-connector-backend-package)
[Section titled “ILP connector (backend package)”](#ilp-connector-backend-package)
Save the following file as `ilp.config` in the `/etc/nginx/sites-available` directory on your VM:
```sh
server {
server_name ilp.myrafiki.com;
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/ilp.myrafiki.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ilp.myrafiki.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
proxy_http_version 1.1;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade "";
proxy_set_header Connection "";
proxy_set_header Host $server_name;
proxy_set_header Accept-Encoding "";
proxy_cache_bypass $http_upgrade;
proxy_pass_request_headers on;
proxy_pass http://localhost:3002;
}
}
server {
server_name ilp.myrafiki.com;
listen 80;
if ($host = ilp.myrafiki.com) {
return 301 https://$host$request_uri;
}
return 404;
}
```
### [Open Payments auth server (`auth` package)](#open-payments-auth-server-auth-package)
[Section titled “Open Payments auth server (auth package)”](#open-payments-auth-server-auth-package)
Save the following file as `open_payments_auth_server.config` in the `/etc/nginx/sites-available` directory on your VM:
```sh
server {
server_name auth.myrafiki.com;
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/auth.myrafiki.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/auth.myrafiki.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
proxy_http_version 1.1;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade "";
proxy_set_header Connection "";
proxy_set_header Host $server_name;
proxy_set_header Accept-Encoding "";
proxy_cache_bypass $http_upgrade;
proxy_pass_request_headers on;
proxy_pass http://localhost:3006;
}
}
server {
server_name auth.myrafiki.com;
listen 80;
if ($host = auth.myrafiki.com) {
return 301 https://$host$request_uri;
}
return 404;
}
```
### [Admin (`frontend` package)](#admin-frontend-package)
[Section titled “Admin (frontend package)”](#admin-frontend-package)
Save the following file as `admin.config` in the `/etc/nginx/sites-available` directory on your VM:
```sh
server {
server_name admin.myrafiki.com;
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/admin.myrafiki.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/admin.myrafiki.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
proxy_http_version 1.1;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade "";
proxy_set_header Connection "";
proxy_set_header Host $server_name;
proxy_set_header Accept-Encoding "";
proxy_cache_bypass $http_upgrade;
proxy_pass_request_headers on;
proxy_pass http://localhost:3005;
}
}
server {
server_name admin.myrafiki.com;
listen 80;
if ($host = admin.myrafiki.com) {
return 301 https://$host$request_uri;
}
return 404;
}
```
## [Set up symbolic links](#set-up-symbolic-links)
[Section titled “Set up symbolic links”](#set-up-symbolic-links)
Once the Nginx configuration files have been created, set up symbolic links that will allow Nginx to read those files and redirect the local paths to the exposed domains and ports.
```sh
sudo ln -s /etc/nginx/sites-available/admin.conf /etc/nginx/sites-enabled/admin.conf
sudo ln -s /etc/nginx/sites-available/open_payments_auth_server.conf /etc/nginx/sites-enabled/open_payments_auth_server.conf
sudo ln -s /etc/nginx/sites-available/ilp.conf /etc/nginx/sites-enabled/ilp.conf
sudo ln -s /etc/nginx/sites-available/open_payments_resource_server.conf /etc/nginx/sites-enabled/open_payments_resource_server.conf
```
## [Deploy with Docker Compose](#deploy-with-docker-compose)
[Section titled “Deploy with Docker Compose”](#deploy-with-docker-compose)
Deploy the configured Rafiki services with Docker Compose:
```sh
docker compose up -d
```
# Helm & Kubernetes
This guide explains how to deploy Rafiki using Helm charts on a Kubernetes cluster. Helm is a package manager for Kubernetes that allows you to define, install, and upgrade complex Kubernetes applications through Helm charts.
Rafiki uses the following key components:
* **Tigerbeetle**: High-performance accounting database used for financial transaction processing and ledger management
* **PostgreSQL**: Used for storing application data and metadata
* **Redis**: Used for caching and messaging between components
## [Prerequisites](#prerequisites)
[Section titled “Prerequisites”](#prerequisites)
Recommended software version
We recommended using the latest vendor supported version for each of the software dependencies listed in this section.
Before you begin, ensure you have the following:
* [Kubernetes](https://kubernetes.io/) cluster deployed
* [kubectl](https://kubernetes.io/docs/tasks/tools/) installed and configured
* [Helm](https://helm.sh/docs/intro/install/) installed
## [Install Rafiki using Helm](#install-rafiki-using-helm)
[Section titled “Install Rafiki using Helm”](#install-rafiki-using-helm)
#### [Add the Interledger Helm repository](#add-the-interledger-helm-repository)
[Section titled “Add the Interledger Helm repository”](#add-the-interledger-helm-repository)
Add the official Interledger Helm repository which contains the Rafiki charts:
```bash
helm repo add interledger https://interledger.github.io/charts
helm repo update
```
#### [Create yaml file](#create-yaml-file)
[Section titled “Create yaml file”](#create-yaml-file)
Create a `values.yaml` file to customize your Rafiki deployment.
Note
A template file is in progress and will be included in this section when published.
#### [Install Rafiki](#install-rafiki)
[Section titled “Install Rafiki”](#install-rafiki)
Install Rafiki using the following command:
```bash
helm install rafiki interledger/rafiki -f values.yaml
```
This will deploy all Rafiki components to your Kubernetes cluster with the configurations specified in your `values.yaml` file.
If you want to install to a specific namespace:
```bash
kubectl create namespace rafiki
helm install rafiki interledger/rafiki -f values.yaml -n rafiki
```
#### [Verify the deployment](#verify-the-deployment)
[Section titled “Verify the deployment”](#verify-the-deployment)
Check the status of your deployment with the following commands:
```bash
# Check all resources deployed by Helm
helm status rafiki
# Check the running pods
kubectl get pods
# Check the deployed services
kubectl get services
```
## [Configure ingress with NGINX Ingress Controller](#configure-ingress-with-nginx-ingress-controller)
[Section titled “Configure ingress with NGINX Ingress Controller”](#configure-ingress-with-nginx-ingress-controller)
Note
This example demonstrates ingress using NGINX. The exact steps and commands will differ depending on the product you use for ingress in your Kubernetes environment.
To expose Rafiki services outside the cluster using NGINX Ingress Controller:
#### [Install NGINX Ingress Controller](#install-nginx-ingress-controller)
[Section titled “Install NGINX Ingress Controller”](#install-nginx-ingress-controller)
If you don’t already have NGINX Ingress Controller installed, you can install it using Helm:
```bash
# Add the ingress-nginx repository
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
# Install the ingress-nginx controller
helm install nginx-ingress ingress-nginx/ingress-nginx \
--set controller.publishService.enabled=true
```
Wait for the Load Balancer to be provisioned:
```bash
kubectl get services -w nginx-ingress-ingress-nginx-controller
```
#### [Configure DNS](#configure-dns)
[Section titled “Configure DNS”](#configure-dns)
Once the Load Balancer has an external IP or hostname assigned, create DNS records:
* `auth.example.com` pointing to the Load Balancer IP/hostname
* `backend.example.com` pointing to the Load Balancer IP/hostname
Note
The example domain and subdomain values are for demonstration purposes only. You must use the actual domain names that you set up with your DNS host.
#### [Apply the configuration](#apply-the-configuration)
[Section titled “Apply the configuration”](#apply-the-configuration)
Apply your updated configuration:
```bash
helm upgrade rafiki interledger/rafiki -f values.yaml
```
#### [Verify ingress configuration](#verify-ingress-configuration)
[Section titled “Verify ingress configuration”](#verify-ingress-configuration)
Check if your ingress resources were created correctly:
```bash
kubectl get ingress
```
You should find entries for the auth server and backend API ingress resources.
## [Port forwarding](#port-forwarding)
[Section titled “Port forwarding”](#port-forwarding)
If you don’t want to use ingress to access Rafiki services, you can use port forwarding to directly access the services:
| Service | Port-Forward Command |
| ----------- | -------------------------------------------------------- |
| Auth Server | `kubectl port-forward svc/rafiki-auth-server 3000:3000` |
| Backend API | `kubectl port-forward svc/rafiki-backend-api 3001:3001` |
| Admin UI | `kubectl port-forward svc/rafiki-backend-api 3001:3001` |
| PostgreSQL | `kubectl port-forward svc/rafiki-postgresql 5432:5432` |
| Redis | `kubectl port-forward svc/rafiki-redis-master 6379:6379` |
## [Upgrade Rafiki](#upgrade-rafiki)
[Section titled “Upgrade Rafiki”](#upgrade-rafiki)
To upgrade your Rafiki deployment to a newer version:
```bash
# Update the Helm repository
helm repo update
# Upgrade Rafiki
helm upgrade rafiki interledger/rafiki -f values.yaml
```
## [Uninstall Rafiki](#uninstall-rafiki)
[Section titled “Uninstall Rafiki”](#uninstall-rafiki)
To uninstall Rafiki from your cluster:
```bash
helm uninstall rafiki
```
Note that this won’t delete [Persistent Volume Claims (PVC)](https://kubernetes.io/docs/concepts/storage/persistent-volumes/) created by the PostgreSQL and Redis deployments. If you want to delete them as well:
```bash
kubectl delete pvc -l app.kubernetes.io/instance=rafiki
```
## [Troubleshooting](#troubleshooting)
[Section titled “Troubleshooting”](#troubleshooting)
### [Check pod logs](#check-pod-logs)
[Section titled “Check pod logs”](#check-pod-logs)
If a component isn’t working correctly, you can check its logs:
```bash
# List all pods
kubectl get pods
# Check logs for a specific pod
kubectl logs pod/rafiki-auth-server-0
```
### [Check resources and logs](#check-resources-and-logs)
[Section titled “Check resources and logs”](#check-resources-and-logs)
```bash
# List pods and their status
kubectl get pods
# Check logs for a specific pod
kubectl logs pod/rafiki-auth-server-0
# Get details about a pod
kubectl describe pod/rafiki-auth-server-0
# Check services and their endpoints
kubectl get services
# Check Persistent Volume Claims
kubectl get pvc
# Check ingress resources
kubectl get ingress
```
## [Common issues](#common-issues)
[Section titled “Common issues”](#common-issues)
#### [Database connection errors](#database-connection-errors)
[Section titled “Database connection errors”](#database-connection-errors)
1. Check if PostgreSQL pods are running:
```plaintext
kubectl get pods -l app.kubernetes.io/name=postgresql
```
2. Check PostgreSQL logs:
```plaintext
kubectl logs pod/rafiki-postgresql-0
```
3. Verify that the database passwords match those in your `values.yaml`
#### [Tigerbeetle initialization failures](#tigerbeetle-initialization-failures)
[Section titled “Tigerbeetle initialization failures”](#tigerbeetle-initialization-failures)
1. Check Tigerbeetle logs:
```plaintext
kubectl logs pod/tigerbeetle-0
```
2. Ensure that the PVC for Tigerbeetle has been created correctly
```plaintext
kubectl get pvc -l app.kubernetes.io/name=tigerbeetle
```
3. Verify that the cluster ID is consistent across all components
#### [Ingress issues](#ingress-issues)
[Section titled “Ingress issues”](#ingress-issues)
1. Verify NGINX Ingress Controller is running:
```plaintext
kubectl get pods -n ingress-nginx
```
2. Check if your DNS records are correctly pointing to the ingress controller’s external IP
3. Check the ingress resource:
```plaintext
kubectl get ingress
```
4. Check ingress controller logs:
```plaintext
kubectl logs -n ingress-nginx deploy/nginx-ingress-ingress-nginx-controller
```
5. Verify that TLS secrets exist if HTTPS is enabled:
```plaintext
kubectl get secrets
```
#### [TLS certificate problems](#tls-certificate-problems)
[Section titled “TLS certificate problems”](#tls-certificate-problems)
1. If using cert-manager, check if certificates are properly issued:
```plaintext
kubectl get certificates
```
2. Check certificate status:
```plaintext
kubectl describe certificate [certificate-name]
```
3. Check cert-manager logs:
```plaintext
kubectl logs -n cert-manager deploy/cert-manager
```
#### [Service unavailable](#service-unavailable)
[Section titled “Service unavailable”](#service-unavailable)
1. Check if services are running:
```plaintext
kubectl get services
```
2. Verify pod health:
```plaintext
kubectl describe pod [pod-name]
```
3. Check for resource constraints:
```plaintext
kubectl top pods
```
#### [Connectivity between components](#connectivity-between-components)
[Section titled “Connectivity between components”](#connectivity-between-components)
1. Ensure all required services are running:
```plaintext
kubectl get services
```
2. Verify service endpoints:
```plaintext
kubectl get endpoints
```
3. Test connectivity between pods using temporary debugging pods:
```plaintext
kubectl run -it --rm debug --image=busybox -- sh
# Inside the pod
wget -q -O- http://rafiki-auth-server:3000/health
```
## [Security considerations](#security-considerations)
[Section titled “Security considerations”](#security-considerations)
When deploying Rafiki in production, consider the following security practices:
* **Use secure passwords**: Replace all default passwords with strong, unique passwords
* **Enable TLS**: Use HTTPS for all external communications
* **Implement network policies**: Use Kubernetes network policies to restrict traffic between pods
* **Use RBAC**: Use Kubernetes Role-Based Access Control to limit access to your cluster
* **Use secrets management**: Consider using a secrets management solution
* **Perform regular updates**: Keep your Rafiki deployment updated
## [Backup and recovery](#backup-and-recovery)
[Section titled “Backup and recovery”](#backup-and-recovery)
### [Database backup](#database-backup)
[Section titled “Database backup”](#database-backup)
#### [PostgreSQL backup](#postgresql-backup)
[Section titled “PostgreSQL backup”](#postgresql-backup)
To create a backup of your PostgreSQL database:
```bash
# Forward PostgreSQL port to local machine
kubectl port-forward svc/rafiki-postgresql 5432:5432
# Use pg_dump to create a backup
pg_dump -h localhost -U rafiki -d rafiki > rafiki_pg_backup.sql
```
#### [Tigerbeetle backup](#tigerbeetle-backup)
[Section titled “Tigerbeetle backup”](#tigerbeetle-backup)
Tigerbeetle is designed to be fault-tolerant with its replication mechanism. However, to create a backup of Tigerbeetle data, you can use the following approach:
```bash
# Create a snapshot of the Tigerbeetle PVC
kubectl get pvc tigerbeetle-data-tigerbeetle-0 -o yaml > tigerbeetle-pvc.yaml
# Create a volume snapshot
cat <>SW: 1. GET wallet address
SW-->>B: 200 OK (authServer URL)
B->>RW: 2. GET wallet address
RW-->>B: 200 OK (authServer URL)
Note over B,AS: Non-interactive grant for incoming payment resource
B->>AS: 3. POST grant request (incoming-payment)
AS-->>B: 200 OK (accessToken)
B->>RW: 4. POST /incoming-payments
RW-->>B: 201 Created (incomingPaymentId)
Note over B,AS: Non-interactive grant for quote resource
B->>AS: 5. POST grant request (quote)
AS-->>B: 200 OK (accessToken)
B->>SW: 6. POST /quotes
SW-->>B: 201 Created (quoteId)
Note over B,AS: Interactive grant required for outgoing payment resource
B->>AS: 7. POST grant request (outgoing-payment)
AS-->>B: 200 OK (redirect_uri)
rect rgb(240, 240, 240)
Note over B,AS: Interactive authorization
User interaction required to obtain consent
B->>IdP: Redirect for user consent
IdP-->>B: User consents
B->>AS: 8. POST /continue/{continueId}
end
AS-->>B: 200 OK (accessToken)
B->>SW: 9. POST /outgoing-payments
SW-->>B: 201 Created (outgoingPaymentId)
B->>SW: 10. GET /outgoing-payments/{outgoingPaymentId}
SW-->>B: 200 OK
```
View full diagramDownload diagram
The sequence of steps outlined below corresponds to the interactions depicted in the diagram above:
1. Requests the sender’s wallet address
2. Requests the receiver’s wallet address
3. Requests a grant to create an incoming payment on the receiver’s account
4. Creates an incoming payment on receiver’s account
5. Requests a grant to create and read a quote on the sender’s account
6. Creates a quote on the sender’s account
7. Requests a grant to create and read an outgoing payment on the sender’s account
Note
You have to go through an interaction flow by selecting the `redirect` link in the grant request’s response. More information about the interaction flow can be found [here](https://openpayments.dev/identity/grants/).
8. Continues the grant request
9. Creates an outgoing payment on the sender’s account
10. Fetches the outgoing payment on the sender’s account
#### [Admin APIs](#admin-apis)
[Section titled “Admin APIs”](#admin-apis)
In addition to using Rafiki Admin to interact with the Admin APIs, you can also use the Apollo Explorer on `localhost:3001/graphql` and `localhost:4001/graphql` for Cloud Nine Wallet and Happy Life Bank, respectively, and via the [Bruno collection](https://github.com/interledger/rafiki/tree/main/bruno/collections/Rafiki/Rafiki%20Admin%20APIs). The Bruno collection is configured to use the local environment’s default endpoints.
#### [SPSP](#spsp)
[Section titled “SPSP”](#spsp)
Every wallet address also serves as an SPSP endpoint. A `GET` request with an `Accept` header `application/spsp4+json` will return an SPSP response with STREAM connection details. The following example uses `http://localhost:3000/accounts/gfranklin` as the SPSP endpoint.
```sh
GET http://localhost:3000/accounts/gfranklin HTTP/1.1
Host:backend
Accept:application/spsp4+json
HTTP/1.1 200 OK
Connection:keep-alive
Content-Length:220
Content-Type:application/spsp4+json
Date:Thu, 23 Feb 2023 13:07:24 GMT
Keep-Alive:timeout=5
{
"destination_account": "test.rafiki.viXmy1OVHgvmQakNjX1C6kQM",
"shared_secret": "Rz_vudcg13EPs8ehL2drvZFJS1LJ4Y3EltOI60-lQ78"
}
```
### [Known issues](#known-issues)
[Section titled “Known issues”](#known-issues)
#### [TigerBeetle container exists with code 137](#tigerbeetle-container-exists-with-code-137)
[Section titled “TigerBeetle container exists with code 137”](#tigerbeetle-container-exists-with-code-137)
There is a known [issue](https://docs.tigerbeetle.com/operating/docker/#exited-with-code-137) when running TigerBeetle in Docker. The container exits without logs and simply shows error code 137. To fix this, increase the Docker memory limit. If you run the local Docker playground on a Windows machine via the Windows Subsystem for Linux (WSL), you can increase the memory limit by [configuring](https://learn.microsoft.com/en-us/windows/wsl/wsl-config#example-wslconfig-file) your `.wslconfig` file.
# Testnet
## [The Test Network](#the-test-network)
[Section titled “The Test Network”](#the-test-network)
The [Local Playground](/v1-beta/integration/playground/overview/) is not the only way to test and try out Rafiki. You can also try the Test Network, which is a Rafiki implementation bundled with a wallet and an e-commerce application available online. As it is a test environment, you can experiment integrating with the Interledger network without using real money.
### [Applications](#applications)
[Section titled “Applications”](#applications)
The current applications include:
* [An Interledger test wallet](https://wallet.interledger-test.dev/)
* [An e-commerce application](https://boutique.interledger-test.dev/products)
## [Peering with the Test Network](#peering-with-the-test-network)
[Section titled “Peering with the Test Network”](#peering-with-the-test-network)
If you have installed the [Local Playground](/v1-beta/integration/playground/overview/) you can peer your local Rafiki instance with the remote Test Network. Refer to the [autopeering](/v1-beta/integration/playground/autopeering/) page for more details.
# Assets
An asset represents an item of value that can be transferred via the Interledger Protocol. Assets in Rafiki are added through the Backend Admin API or the [Rafiki Admin](/v1-beta/admin/admin-user-guide/#assets) application.
## [Add an asset](#add-an-asset)
[Section titled “Add an asset”](#add-an-asset)
* Operation
```graphql
mutation CreateAsset($input: CreateAssetInput!) {
createAsset(input: $input) {
code
success
message
asset {
id
code
scale
}
}
}
```
* Variables
```json
{
"input": {
"code": "USD",
"scale": 2
}
}
```
For more information about this mutation’s input object, see [`CreateAssetInput`](/v1-beta/apis/graphql/backend/#definition-CreateAssetInput).
* Response
```json
{
"data": {
"createAsset": {
"code": "200",
"success": true,
"message": "Created Asset",
"asset": {
"id": "b3dffeda-1e0e-47d4-82a3-69b1a622eeb9",
"code": "USD",
"scale": 2
}
}
}
}
```
# Exchange rates
If you plan to support cross-currency transactions, you must specify from where your Rafiki instance will fetch current exchange rates.
A rate probe precedes every Interledger payment. The probe provides a quote that estimates the full cost of transferring value over the network. For a rate probe involving a cross-currency transaction to be successful, Rafiki needs to know the exchange rates for each currency that makes up the transaction.
Often, it’s the receiving ASE that provides the exchange rates for each ILP packet. For example, say you and your peer transact in USD. Your peer also supports MXN. If a USD payment from your side is addressed to a wallet address set up for MXN on your peer’s side, then your peer would provide the USD to MXN exchange rate.
## [Specify your exchange rates endpoint](#specify-your-exchange-rates-endpoint)
[Section titled “Specify your exchange rates endpoint”](#specify-your-exchange-rates-endpoint)
Rafiki fetches exchange rates from your exchange rates endpoint. Set your endpoint via the `backend` service’s `EXCHAGE_RATES_URL` variable. An [OpenAPI specification](https://github.com/interledger/rafiki/blob/main/packages/backend/src/openapi/specs/exchange-rates.yaml) for the endpoint is available.
Example
```bash
EXCHANGE_RATES_URL: http://cloud-nine-wallet/rates
```
The endpoint must accept GET requests and respond as follows.
Example API request
```bash
GET https://cloud-nine-wallet/rates
```
Example API response
```bash
{
"base": "USD",
"rates": {
"EUR": 0.813399,
}
}
```
### [Response objects](#response-objects)
[Section titled “Response objects”](#response-objects)
| Variable | Type | Description | Required |
| -------------------- | ------ | ----------------------------------------------------------------------------------------------------------- | -------- |
| `base` | String | The asset code represented as an [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217) currency code, e.g. USD | Y |
| `rates` | Object | Object containing `` pairs, e.g. `{EUR: 0.8930}` | Y |
| `rates.` | Number | The exchange rate given `base` and `` | Y |
## [Specify rate caching duration (optional)](#specify-rate-caching-duration-optional)
[Section titled “Specify rate caching duration (optional)”](#specify-rate-caching-duration-optional)
Specify how long your Rafiki instance will cache exchange rates via the `backend` service’s `EXCHANGE_RATES_LIFETIME` variable or use the default setting of `15_000` ms (15 seconds).
Caching improves performance as Rafiki will not need to request the rates from your endpoint for every payment.
## [Specify slippage (optional)](#specify-slippage-optional)
[Section titled “Specify slippage (optional)”](#specify-slippage-optional)
As exchange rates and fees charged by connectors fluctuate, there will likely be a variance between the estimated amount provided in the quote and the actual amount required when the payment is initiated. This difference is called slippage.
Set your allowed slippage rate to a value between 0 and 1 via the `backend` service’s `SLIPPAGE` variable or use the default setting of `0.01` (1%).
Example
```bash
SLIPPAGE: 0.05
```
### [Example](#example)
[Section titled “Example”](#example)
Let’s say your Rafiki instance is using the default slippage of `0.01` (1%). The rate probe that precedes a USD payment returns a quote of `$1.00`. One percent of one dollar equals one cent.
If the total of the payment, inclusive of currency exchange rates and network fees, amounts to `$1.01`, the payment will be successful. If the total is `$1.02` or more, the payment will fail.
Below is a minimalistic example of a successful (200) response.
```json
export function loader({ request }: LoaderFunctionArgs) {
const base = new URL(request.url).searchParams.get('base') || 'USD'
return json(
{
base,
rates: config.seed.rates[base] || {}
},
{ status: 200 }
)
}
```
## [Environment variables](#environment-variables)
[Section titled “Environment variables”](#environment-variables)
| Variable | Type | Description | Required |
| ------------------------- | --------- | ----------------------------------------------------------------------------------------------- | -------- |
| `EXCHANGE_RATES_URL` | `backend` | Your exchange rates endpoint | Y |
| `EXCHANGE_RATES_LIFETIME` | `backend` | The amount of time Rafiki caches exchange rates, in ms | Y |
| `SLIPPAGE` | `backend` | The variance allowed between a quote and the actual amount required when a payment is initiated | Y |
# Viewing and revoking grants
Grants are the mechanism in Open Payments by which your account holders give permission to a client application to access their accounts and send payments on their behalf. Providing your account holders the ability to view and revoke grants is not required to implement and operate Rafiki, but allowing them to do so is critical to providing an optimal user experience.
## [View grants](#view-grants)
[Section titled “View grants”](#view-grants)
Use the `Grants` GraphQL query to look up all grants associated with a wallet address.
* Operation
```graphql
query Grants(
$after: String
$before: String
$first: Int
$last: Int
$filter: GrantFilter
) {
grants(
after: $after
before: $before
first: $first
last: $last
filter: $filter
) {
edges {
cursor
node {
id
client
createdAt
state
access {
createdAt
id
identifier
limits {
interval
receiveAmount {
assetScale
value
assetCode
}
receiver
debitAmount {
assetCode
assetScale
value
}
}
actions
type
}
}
}
pageInfo {
endCursor
hasNextPage
hasPreviousPage
startCursor
}
}
}
```
* Variables
```json
{
"input": {
"after": null,
"before": null,
"first": null,
"last": null,
"filter": {
"state": {
"in": ["PROCESSING", "PENDING", "APPROVED", "FINALIZED"]
},
"identifier": {
"in": ["https://cloud-nine-wallet-backend/accounts/gfranklin"]
}
}
}
}
```
For more information about this query’s variables, see [`grants`](/v1-beta/apis/graphql/auth/#query-grants).
* Response
```json
{
"data": {
"grants": {
"edges": [
{
"cursor": "82637448-30d2-4242-9c85-464821dfbaf5",
"node": {
"id": "82637448-30d2-4242-9c85-464821dfbaf5",
"client": "https://happy-life-bank-backend/accounts/pfry",
"createdAt": "2025-03-27T13:48:23.615Z",
"state": "APPROVED",
"access": [
{
"createdAt": "2025-03-27T13:48:23.617Z",
"id": "05a5413b-7009-4ce1-949a-e0ff1b243268",
"identifier": "https://cloud-nine-wallet-backend/accounts/gfranklin",
"limits": {
"interval": null,
"receiveAmount": {
"assetScale": 2,
"value": "100",
"assetCode": "USD"
},
"receiver": null,
"debitAmount": {
"assetCode": "USD",
"assetScale": 2,
"value": "205"
}
},
"actions": [
"create",
"read",
"list"
],
"type": "outgoing-payment"
}
]
}
}
],
"pageInfo": {
"endCursor": "82637448-30d2-4242-9c85-464821dfbaf5",
"hasNextPage": false,
"hasPreviousPage": false,
"startCursor": "82637448-30d2-4242-9c85-464821dfbaf5"
}
}
}
}
```
## [Revoke a grant](#revoke-a-grant)
[Section titled “Revoke a grant”](#revoke-a-grant)
Use the `revokeGrant` GraphQL mutation to revoke a particular grant.
* Operation
```graphql
mutation revokeGrant($input: RevokeGrantInput!) {
revokeGrant(input: $input) {
id
}
}
```
* Variables
```json
{
"input": {
"grantId": "2117891e-4b89-42ae-984e-e0762d5888c1"
}
}
```
For more information about this mutation’s input object, see [`RevokeGrantInput`](/v1-beta/apis/graphql/auth/#definition-RevokeGrantInput).
* Response
```json
{
"data": {
"revokeGrant": {
"id": "2117891e-4b89-42ae-984e-e0762d5888c1"
}
}
}
```
# Identity provider (IdP)
An identity provider (IdP) is a system or service that stores and manages user identity information, authentication, and consent. Examples of IdPs include OpenID Connect and Okta.
Open Payments requires any authorization server that issues interactive grants be integrated with an IdP. Interactive grants are used to gather consent. More information about interactive grants is available [below](#interactive-grants).
Responsibilities of your IdP include:
* Providing an interface to gather end-user consent for a particular action
* Sending the interaction choice (approve or deny) to the authorization server
* Sending a request to the authorization server to finish the interaction
* Redirecting the user after the interaction is complete
Note
We provide Ory Kratos, a cloud-based user management system, for the identity and user management of your Rafiki Admin users. Kratos is for internal use only and **cannot** be used as your IdP for Open Payments.
## [Interactive grants](#interactive-grants)
[Section titled “Interactive grants”](#interactive-grants)
In Open Payments, grants indicate a resource owner, such as an account holder, has given a piece of software, such as a mobile app, permission (consent) to act on their behalf.
Rafiki’s implementation of an Open Payments authorization server requires that consent is collected via an interactive grant before an [outgoing payment](https://openpayments.dev/concepts/resources/#outgoing-payment) request is issued. A grant is interactive when explicit interaction by a resource owner (for example, the software’s end user) is required to approve or deny the grant. Tapping an *Approve* button to authorize a payment is an example of an explicit interaction.
Interactive grants can be optional for incoming payments and quotes; however, they’re enabled by default in Rafiki (the `LIST_ALL_ACCESS_INTERACTION` environment variable is `true`). When a grant request includes a `list-all` action for incoming payments and quotes, the request requires interaction. The `list-all` action is used when the client asks to list resources that it did not create.
If `LIST_ALL_ACCESS_INTERACTION` is `false`, you can still force interactive grants for quotes and/or incoming payments by setting the respective variables to `true`.
* `QUOTE_INTERACTION`
* `INCOMING_PAYMENT_INTERACTION`
See the [Open Payments documentation](https://openpayments.dev/identity/grants/) for more information on grant negotiation and authorization.
## [Authorization servers](#authorization-servers)
[Section titled “Authorization servers”](#authorization-servers)
Authorization servers grant permission to clients to access the Open Payments Resource APIs. This enables clients to create incoming payments, quotes, and outgoing payments against an account holder’s account.
Rafiki’s [auth service](/v1-beta/integration/deployment/services/auth-service) provides you with a reference implementation of an Open Payments authorization server. You can use the service as an alternative to developing your own in-house service.
Rafiki’s authorization server also extends an [API](#interaction-endpoints) that provides interaction endpoints for your IdP.
### [Environment variables](#environment-variables)
[Section titled “Environment variables”](#environment-variables)
The following variables must be configured for the `auth` service.
| Variable | Helm value name | Default | Description |
| ------------------------------ | ---------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `IDENTITY_SERVER_URL` | `auth.identityServer.domain` | N/A | The URL of your IdP’s server, used by the authorization server to inform an Open Payments client of where to redirect the end-user to start interactions. |
| `IDENTITY_SERVER_SECRET` | `auth.identityServer.secret` | N/A | A shared secret between the authorization server and the IdP server; the authorization server uses the secret to secure its IdP-related endpoints. When the IdP server sends requests to the authorization server, the IdP server must provide the secret via an [`x-idp-secret`](#x-idp-secret-header) header. |
| `INCOMING_PAYMENT_INTERACTION` | `auth.interaction.incomingPayment` | `false` | Indicates whether incoming payments grant requests are interactive. |
| `INTERACTION_EXPIRY_SECONDS` | `auth.interactionExpirySeconds` | `600` | The time in seconds for which a user can interact with a grant request |
| `INTERACTION_PORT` | `auth.port.interaction` | `3009` | The port number for the [interaction endpoints](#interaction-endpoints) |
| `LIST_ALL_ACCESS_INTERACTION` | N/A | `true` | Specifies whether grant requests including a `list-all` action should require interaction. In these requests, the client asks to list resources that they themselves did not create. |
| `QUOTE_INTERACTION` | `auth.interaction.quote` | `false` | When `true`, quote grants are interactive. |
## [Interaction endpoints](#interaction-endpoints)
[Section titled “Interaction endpoints”](#interaction-endpoints)
The authorization server provided by Rafiki’s `auth` service extends an API for an IdP server to use after a pending grant request is created.
Each interaction with an endpoint is identified by an `id` and a `nonce`. Both are provided as query parameters when the authorization server redirects to the IdP server.
The endpoints are tied to the auth server URL. For example, if your auth server URL is `https://auth.wallet.example.com`, then calling the `/interact/{id}/{nonce}` endpoint to start a user interaction session would look as follows:
```plaintext
https://auth.wallet.example.com/interact/{id}/{nonce}
```
### [Interaction endpoints](#interaction-endpoints-1)
[Section titled “Interaction endpoints”](#interaction-endpoints-1)
The endpoints are called in the sequence listed below.
| Method | Endpoint | Purpose | Called by | Publicly exposed |
| ------ | ------------------------------- | ----------------------------------------------------------------- | -------------------- | ---------------- |
| GET | `/interact/{id}/{nonce}` | [Start user interaction session](#start-user-interaction-session) | Open Payments client | Yes |
| GET | `/grant/{id}/{nonce}` | [Look up grant information](#look-up-grant-information) | Identity provider | No |
| POST | `/grant/{id}/{nonce}/{choice}` | [Accept or reject grant](#accept-or-reject-grant) | Identity provider | No |
| GET | `/interact/{id}/{nonce}/finish` | [Finish user interaction](#finish-interaction) | Identity provider | Yes |
| POST | `/interact/{id}/{nonce}` | [Continue grant](#continue-grant) | Open Payments client | Yes |
We also provide an [OpenAPI specification](https://github.com/interledger/rafiki/blob/main/packages/auth/src/openapi/specs/id-provider.yaml) that describes the endpoints. Note that the *Continue grant* endpoint is not included in the spec because it’s part of the [Open Payments Auth Server API](https://openpayments.dev/apis/auth-server/operations/post-continue/).
#### [Start user interaction session](#start-user-interaction-session)
[Section titled “Start user interaction session”](#start-user-interaction-session)
Called by the client to establish an interactive session with the authorization server. The authorization server automatically redirects the request, via the URL defined in the `IDENTITY_SERVER_URL` variable, to your IdP consent screen.
#### [Look up grant information](#look-up-grant-information)
[Section titled “Look up grant information”](#look-up-grant-information)
Called by the IdP server to retrieve a list of access rights, requested by the client, from the authorization server. The request is secured with an [`x-idp-secret`](#x-idp-secret-header) header. The access rights are presented to the client’s end-user on the consent screen. The authorization server’s response is served on your configured `INTERACTION_PORT`.
#### [Accept or reject grant](#accept-or-reject-grant)
[Section titled “Accept or reject grant”](#accept-or-reject-grant)
The IdP server communicates the choice made by the end-user on the consent screen (accept/reject) to the authorization server. The request is secured with an [`x-idp-secret`](#x-idp-secret-header) header. The authorization server responds to the IdP server, acknowledging that it received the request.
#### [Finish interaction](#finish-interaction)
[Section titled “Finish interaction”](#finish-interaction)
Called by the IdP server to end the interaction. If a `finish` URI was provided in the original grant initialization request, the authorization server redirects the user to that URI.
The `result` query parameter in the response indicates the success or failure of the grant authorization. The following are examples of the possible response types.
| Response | Description | Example |
| -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| Rejected | The end-user rejected the interaction | `?result=grant_rejected` |
| Invalid | The grant was not in a state where it could be accepted or rejected (for example, the grant was already approved) | `?result=grant_invalid` |
| Success | The grant was successful with the following returned in the response: - A hash representing the SHA-256 hash of values provided by the client in the [grant initialization request](https://openpayments.dev/apis/auth-server/operations/post-request/) (`interact.finish.nonce`), and the values in the response returned from the authorization server (`interact.finish`).
- The `interact_ref` that identifies the interaction on the authorization server alongside the hash
- The URI of the grant initialization request (for example, `https://www.auth-server.com`) | `hash=p28jsq0Y2KK3WS__a42tavNC64ldGTBroywsWxT4md_jZQ1R\HZT8BOWYHcLmObM7XHPAdJzTZMtKBsaraJ64A &interact_ref=4IFWWIKYBC2PQ6U56NL1` |
When successful, the SHA-256 hash of the interaction is sent in the response to the client, along with an `interact_ref` that identifies the interaction on the authorization server and the URI of the grant initialization request. The client must verify the hash before the client requests the grant to continue.
#### [Continue grant](#continue-grant)
[Section titled “Continue grant”](#continue-grant)
The client requests a grant from the authorization server for an accepted interaction. The authorization server responds with an [access token](https://openpayments.dev/apis/auth-server/operations/post-continue/).
## [X-idp-secret header](#x-idp-secret-header)
[Section titled “X-idp-secret header”](#x-idp-secret-header)
The `x-idp-secret` header is specific to Rafiki’s authorization server and is used for requests to the following endpoints:
* `GET /grant/:id/:nonce`
* `POST /grant/:id/:nonce/accept`
* `POST /grant/:id/:nonce/reject`
The header’s purpose is to secure communications between the IdP and the authorization server. Its value should be a shared secret known to both entities. When the IdP server sends requests to the authorization server, the IdP must provide the secret via this header.
Note
If you’re running your own authorization server rather than using the server provided by Rafiki, you can add security in any way you see fit. You aren’t required to use the `x-idp-secret` header.
To set up the header, set the `IDENTITY_SERVER_SECRET` variable to a value that is also used to configure your IdP server’s requests to the authorization server.
# Wallet address keys
Creating a public-private key pair for each wallet address is not required when integrating with Rafiki.
You only need to create key pairs for wallet addresses if you want to allow your account holders to use/be Open Payments clients under their wallet addresses. For more information, review the Open Payments documentation about [clients](https://openpayments.dev/resources/glossary/#client) and [client keys](https://openpayments.dev/identity/client-keys/).
## [Create a wallet address key pair](#create-a-wallet-address-key-pair)
[Section titled “Create a wallet address key pair”](#create-a-wallet-address-key-pair)
Use the `createWalletAddressKey` GraphQL mutation to create a key pair and associate it with a wallet address.
* Operation
```graphql
mutation CreateWalletAddressKey($input: CreateWalletAddressKeyInput!) {
createWalletAddressKey(input: $input) {
code
message
success
walletAddressKey {
id
walletAddressId
revoked
jwk {
alg
crv
kid
kty
x
}
createdAt
}
}
}
```
* Variables
```json
{
"input": {
"jwk": {
"kid": "keyid-97a3a431-8ee1-48fc-ac85-70e2f5eba8e5",
"x": "ubqoInifJ5sssIPPnQR1gVPfmoZnJtPhTkyMXNoJF_8",
"alg": "EdDSA",
"kty": "OKP",
"crv": "Ed25519"
},
"walletAddressId": "695e7546-1803-4b45-96b6-6a53f4082018"
}
}
```
The request is a standard request to create a JSON Web Key (JWK), which is a JSON data structure that represents a cryptographic key. [Section 4](https://datatracker.ietf.org/doc/html/rfc7517#section-4) of the JWK specification describes the format and associated parameters `kty`, `alg`, and `kid`. [Section 6](https://datatracker.ietf.org/doc/html/rfc7518#section-6) of the JSON Web Algorithms (JWA) specification describes the cryptographic algorithm for the keys and associated parameters `kty`, `crv`, and `x`.
Open Payments [requires](https://openpayments.dev/apis/wallet-address-server/operations/get-wallet-address-keys/) the following values.
| Parameter | Required value | Description |
| --------- | -------------- | ----------------------------------------------------------------------------- |
| `alg` | `EdDSA` | The algorithm used to generate the key pair |
| `kty` | `OKP` | The key type identifying the cryptographic algorithm family used with the key |
| `crv` | `Ed25519` | The cryptographic curve used with the key |
Additionally, the request must contain the `walletAddressId` of the wallet address that the key pair will be associated with.
* Response
```json
{
"data": {
"createWalletAddressKey": {
"code": "200",
"message": "Added Key To Wallet Address",
"success": true,
"walletAddressKey": {
"id": "f2953571-f10c-44eb-ab41-4450a7ad6771",
"walletAddressId": "695e7546-1803-4b45-96b6-6a53f4082018",
"revoked": false,
"jwk": {
"alg": "EdDSA",
"crv": "Ed25519",
"kid": "keyid-97a3a431-8ee1-48fc-ac85-70e2f5eba8e5",
"kty": "OKP",
"x": "ubqoInifJ5sssIPPnQR1gVPfmoZnJtPhTkyMXNoJF_8"
},
"createdAt": "2023-03-03T09:26:41.424Z"
}
}
}
}
```
## [Revoke a wallet address key](#revoke-a-wallet-address-key)
[Section titled “Revoke a wallet address key”](#revoke-a-wallet-address-key)
Use the `revokeWalletAddressKey` GraphQL mutation to revoke a public key associated with a wallet address. Open Payments requests using this key for request signatures will be denied going forward.
* Operation
```graphql
mutation RevokeWalletAddressKey($input: RevokeWalletAddressKeyInput!) {
revokeWalletAddressKey(input: $input) {
walletAddressKey {
id
revoked
walletAddressId
createdAt
}
}
}
```
* Variables
```json
{
"input": {
"id": "e7532552-cff9-4ffe-883e-56613d3ae611"
}
}
```
* Response
```json
{
"data": {
"revokeWalletAddressKey": {
"walletAddressKey": {
"id": "f2953571-f10c-44eb-ab41-4450a7ad6771",
"revoked": true,
"walletAddressId": "695e7546-1803-4b45-96b6-6a53f4082018",
"createdAt": "2023-03-03T09:26:41.424Z"
}
}
}
}
```
# Requirements overview and integration checklist
You must meet the following requirements before you deploy Rafiki to a production environment.
## [Be an account servicing entity Required](#be-an-account-servicing-entity-)
[Section titled “Be an account servicing entity ”](#be-an-account-servicing-entity-)
An account servicing entity (ASE) is an entity that provides and maintains payment accounts for its customers and is regulated within the jurisdictions it operates. Examples of ASEs include banks, digital wallet providers, and mobile money providers. The [account servicing entity](/v1-beta/overview/concepts/account-servicing-entity) page provides examples of an ASE’s responsibilities and obligations.
Rafiki should not be used in production by non-regulated entities.
## [Support at least one asset Required](#support-at-least-one-asset-)
[Section titled “Support at least one asset ”](#support-at-least-one-asset-)
You must set up Rafiki to support at least one asset. An asset in Rafiki represents an item of value that can be transferred via the Interledger Protocol. Since the Interledger Protocol aims to create an internet of value, it allows for the transfer of any asset, not just currency. In practice, however, assets are usually denominated in a currency (fiat or branded currencies).
[Set up your assets](/v1-beta/integration/requirements/assets)
## [Associate each user-facing payment account with a wallet address Required](#associate-each-user-facing-payment-account-with-a-wallet-address-)
[Section titled “Associate each user-facing payment account with a wallet address ”](#associate-each-user-facing-payment-account-with-a-wallet-address-)
A wallet address is a publicly shareable standardized ID for a payment account. Each payment account belonging to your users (for example, your customers) must have at least one associated wallet address for the account to be able to send and/or receive payments via Open Payments and Interledger.
[Set up your wallet addresses](/v1-beta/integration/requirements/wallet-addresses)
## [Expose a webhook endpoint and react to events accordingly Required](#expose-a-webhook-endpoint-and-react-to-events-accordingly-)
[Section titled “Expose a webhook endpoint and react to events accordingly ”](#expose-a-webhook-endpoint-and-react-to-events-accordingly-)
The main communication channel between you and your Rafiki instance is composed of the Backend Admin API and a set of webhook events. Most of these events require you to interact with Rafiki. You must expose a webhook endpoint that listens for events dispatched by Rafiki, then react accordingly (for example, deposit or withdraw liquidity).
[Specify your webhook endpoint and learn how to handle each event](/v1-beta/integration/requirements/webhook-events)
## [Expose an exchange rate endpoint Conditionally Optional](#expose-an-exchange-rate-endpoint-)
[Section titled “Expose an exchange rate endpoint ”](#expose-an-exchange-rate-endpoint-)
If you plan to support cross-currency transactions, you must specify from where your Rafiki instance will fetch current exchange rates. Exchange rates are calculated as part of a payment’s quote, which estimates the full cost of transferring value over the network.
[Specify your exchange rate endpoint](/v1-beta/integration/requirements/exchange-rates)
## [Define your sending fees Optional](#define-your-sending-fees-)
[Section titled “Define your sending fees ”](#define-your-sending-fees-)
You have the option to charge a sending fee, on top of any estimated network fees, for facilitating transfers. Each asset you support can have a different fee structure.
[Define your sending fees](/v1-beta/integration/requirements/sending-fees)
## [Add a peer to enable Interledger payments Conditionally Optional](#add-a-peer-to-enable-interledger-payments-)
[Section titled “Add a peer to enable Interledger payments ”](#add-a-peer-to-enable-interledger-payments-)
You must add one or more peers if you intend to enable Interledger payments on your accounts. A peer is another ASE that you connect with via Interledger and is likely running their own Rafiki instance.
If you are using Rafiki solely for transfers between accounts on your ledger, peers are not required.
[Add peers to your Rafiki instance](/v1-beta/integration/requirements/peers)
## [Integrate with an identity provider (IdP) Conditionally Optional](#integrate-with-an-identity-provider-idp-)
[Section titled “Integrate with an identity provider (IdP) ”](#integrate-with-an-identity-provider-idp-)
An identity provider (IdP) is a system or service that stores and manages user identity information, authentication, and consent. Examples of IdPs include OpenID Connect and Okta.
You must integrate with an IdP if you plan to use the authorization server provided through Rafiki’s auth service. The authorization server requires consent be collected via an interactive grant before an outgoing payment request is issued. The purpose of the IdP is to handle the authentication and consent required to authorize the interactive grant request.
[Integrate Rafiki with your IdP](/v1-beta/integration/requirements/open-payments/idp)
## [Integration checklist](#integration-checklist)
[Section titled “Integration checklist”](#integration-checklist)
Ensure you’ve completed the following tasks before you deploy Rafiki to a production environment and join the Interledger network.
* [ ] You are a licensed financial account servicing entity within the jurisdictions you operate in
* [ ] You have added at least one asset, either through the Backend Admin API or the Rafiki Admin app
* [ ] You have implemented a strategy for creating wallet addresses for your account holders
* [ ] You have set up your webhook endpoint and understand how to handle each webhook event
* [ ] You have set up your exchange rates endpoint
* [ ] You have defined the sending fee you will charge, if any, for each asset, either through the Backend Admin API or the Rafiki Admin app
* [ ] If supporting Open Payments outgoing payments, you have integrated with an IdP and configured the user consent screen and interaction flow
* [ ] Your admin services are secured from external access
# Peers
To join the Interledger network and be able to send and receive payments, you must add one or more peers to your Rafiki instance. Peering establishes the connections needed for your Rafiki instance to interact with another account servicing entity (ASE). The purpose of this guide is to help you set up and manage peers.
While this guide focuses on the conceptual and technical steps of adding and managing peers via the Backend Admin API, the Rafiki Admin application offers the same capabilities in a user-friendly interface.
Refer to the [Rafiki Admin user guide](/v1-beta/admin/admin-user-guide#peers) for detailed instructions and examples of creating and managing peers through the application.
Tip
Whether you are using the Backend Admin API or the Rafiki Admin application, the underlying configurations and requirements remain the same. Choose the interface that best suits your individual workflow.
## [Perform prerequisites](#perform-prerequisites)
[Section titled “Perform prerequisites”](#perform-prerequisites)
Note
Peering is not required unless you want to participate in transactions with another ASE on the Interledger network. For foundational peering concepts, refer to the Peers section of [Interledger Concepts](/v1-beta/overview/concepts/interledger/#peers).
Before adding a peer, you and the account servicing entity you intend to peer with must both:
### [Run an Interledger connector](#run-an-interledger-connector)
[Section titled “Run an Interledger connector”](#run-an-interledger-connector)
While you and your peer may run any implementation of an [Interledger connector](/v1-beta/integration/deployment/services/backend-service#interledger-connector) such as the [TypeScript implementation](https://github.com/interledger/interledgerjs), it is recommended to use Rafiki.
### [Agree on an asset](#agree-on-an-asset)
[Section titled “Agree on an asset”](#agree-on-an-asset)
Both you and your peer must agree on an [asset](/v1-beta/overview/concepts/accounting#assets) for the peering relationship. You can set up multiple peering relationships with the same peer based on different assets. At least one asset shared by you and your peer must be added to your Rafiki instance prior to setting up the peering relationship. For more information, refer to [Assets](/v1-beta/integration/requirements/assets/).
### [Exchange static Interledger (ILP) addresses](#exchange-static-interledger-ilp-addresses)
[Section titled “Exchange static Interledger (ILP) addresses”](#exchange-static-interledger-ilp-addresses)
Your [ILP address](https://interledger.org/developers/rfcs/ilp-addresses/) is self-assigned during Rafiki setup and stored locally as the `ILP_ADDRESS` environment variable for the `backend` service.
### [Communicate a connection endpoint](#communicate-a-connection-endpoint)
[Section titled “Communicate a connection endpoint”](#communicate-a-connection-endpoint)
The connection endpoint will be a url that the other peer will send packets to.
### [Exchange auth tokens for the connection endpoint](#exchange-auth-tokens-for-the-connection-endpoint)
[Section titled “Exchange auth tokens for the connection endpoint”](#exchange-auth-tokens-for-the-connection-endpoint)
Incoming `authtokens` allow you to authenticate that packets sent from your peer originated from your peer’s Interledger connector and were not tampered en route. The outgoing `authtoken` allows your peer to authenticate that received packets originated from your Interledger connector and were not tampered with en route. The use of auth tokens is not required when [autopeering with the Test Network](/v1-beta/integration/playground/autopeering).
### [Agree on a settlement mechanism](#agree-on-a-settlement-mechanism)
[Section titled “Agree on a settlement mechanism”](#agree-on-a-settlement-mechanism)
The settlement mechanism you both agree to use is what facilitates the transfer of actual funds between you and your peer. Neither Interledger nor Rafiki provide a settlement mechanism.
## [Perform optional prerequisites](#perform-optional-prerequisites)
[Section titled “Perform optional prerequisites”](#perform-optional-prerequisites)
### [Deposit an initial liquidity for your peer](#deposit-an-initial-liquidity-for-your-peer)
[Section titled “Deposit an initial liquidity for your peer”](#deposit-an-initial-liquidity-for-your-peer)
While you may deposit an `initiaLiquidity` for your peer, you can deposit liquidity later using the `depositPeerLiquidity` mutation.
### [Define a maxPacketAmout value](#define-a-maxpacketamout-value)
[Section titled “Define a maxPacketAmout value”](#define-a-maxpacketamout-value)
The `maxPacketAmount` specifies the maximum packet size you are willing to accept from the peer. Your peer’s `maxPacketAmount` value does not need to match, as this value is independently set by each ASE. If omitted, payments will not be broken into smaller packets.
## [Set up peering in Rafiki](#set-up-peering-in-rafiki)
[Section titled “Set up peering in Rafiki”](#set-up-peering-in-rafiki)
The basic workflow of setting up a peering relationship starts with adding the agreed upon asset and then adding a peer.
### [Add an asset](#add-an-asset)
[Section titled “Add an asset”](#add-an-asset)
As mentioned in the prerequisites, you must add an asset to your Rafiki instance before creating a peering relationship. To learn how to add an asset, refer to [Assets](/v1-beta/integration/requirements/assets/).
### [Add a peer](#add-a-peer)
[Section titled “Add a peer”](#add-a-peer)
* Operation
```graphql
mutation CreatePeer($input: CreatePeerInput!) {
createPeer(input: $input) {
code
success
message
peer {
id
asset {
code
scale
}
staticIlpAddress
name
}
}
}
```
* Variables
```json
{
"input": {
"staticIlpAddress": "g.othergreatwallet",
"name": "The Other Great Wallet"
"http": {
"incoming": {"authTokens": ["mytoken"]},
"outgoing": {"endpoint": "ilp.othergreatwallet.com", "authToken": "theirtoken"}
},
"assetId": "INSERT_ASSET_ID",
"initialLiquidity":
}
}
```
For more information about this mutation’s input object, see [`CreatePeerInput`](/v1-beta/apis/graphql/backend/#definition-CreatePeerInput).
* Response
```json
{
"data": {
"createPeer": {
"code": "200",
"success": true,
"message": "Created ILP Peer",
"peer": {
"id": "480ef339-7842-4501-a905-923fc1339cef",
"asset": {
"code": "USD",
"scale": 2
},
"staticIlpAddress": "g.othergreatwallet",
"name": "The Other Great Wallet"
}
}
}
}
```
## [Manage peers](#manage-peers)
[Section titled “Manage peers”](#manage-peers)
Once a peer has been added to your Rafiki instance, there is minimal ongoing management required. Most peer interactions focus on monitoring liquidity and ensuring smooth payment flows. In rare cases, you may need to update a peer’s configuration due to changes in their technical details or remove a peer created in error, as long as no payments have been exchanged. These actions help ensure your Rafiki instance stays up to date with operational changes.
### [Edit a peer](#edit-a-peer)
[Section titled “Edit a peer”](#edit-a-peer)
Occasionally, you may need to adjust peering configurations or address any changes communicated by the peer. Some examples include updating new endpoints or tokens, technical settings like the maximum packet amount, or peer liquidity thresholds.
In this example we will update the peer we just created. Rather than change any of the peering details, we can add some optional details that we didn’t include when we created the peer. We will define the `maxPacketAmount` and the `liquidityThreshold`.
* Operation
```graphql
mutation UpdatePeer($input: UpdatePeerInput!) {
updatePeer(input: $input) {
peer {
id
name
http {
outgoing {
authToken
endpoint
}
}
maxPacketAmount
liquidityThreshold
}
}
}
```
* Variables
The input object for the update operation only requires that the `id` is present. All other variables are optional. For this example we will include the required `id` variable, as well as the optional variables of the fields we wish to update. In this case, `maxPacketAmount` and `liquidityThreshold`..
```json
{
"input": {
"id": "480ef339-7842-4501-a905-923fc1339cef",
"maxPacketAmount": 1000,
"liquidityThreshold": 100
}
}
```
For more information about this mutation’s input object, see [`UpdatePeerInput`](/v1-beta/apis/graphql/backend/#definition-UpdatePeerInput).
* Response
```json
{
"data": {
"updatePeer": {
"code": "200",
"success": true,
"message": "Updated ILP Peer",
"peer": {
"id": "480ef339-7842-4501-a905-923fc1339cef",
"name": "The Other Great Wallet",
"http": {
"outgoing": {
"authToken": "test",
"endpoint": "http://peering-test:3002"
}
},
"maxPacketAmount": 1000,
"liquidityThreshold": 100
}
}
}
}
```
### [Delete a peer](#delete-a-peer)
[Section titled “Delete a peer”](#delete-a-peer)
Deleting a peer is an action that removes a peer from your Rafiki instance. There are specific rules and considerations to keep in mind before starting this irreversible operation.
You can only delete a peer if no payments were sent to or received from that peer. This ensures that historical payment records are preserved. If you attempt to delete a peer with payment history, the backend throws an error, preventing the deletion.
Deleting a peer is useful in situations where there were configuration errors when the peer was first created like an incorrect auth token or ILP address.
Danger
Deleting a peer is permanent and cannot be reversed. If you delete a peer in error, you must create another new peer.
* Operation
```graphql
mutation DeletePeer($input: DeletePeerInput!) {
deletePeer(input: $input) {
success
}
}
```
* Variables
```json
{
"input": {
"id": "480ef339-7842-4501-a905-923fc1339cef"
}
}
```
For more information about this mutation’s input object, see [`DeletePeerInput`](/v1-beta/apis/graphql/backend/#definition-DeletePeerInput).
* Response
```json
{
"data": {
"deletePeer": {
"success": true
}
}
}
```
# Sending fees
You have the option to charge sending fees, on top of any estimated network fees, for facilitating transfers. Each asset you support can have a different fee structure and you can specify both fixed and variable fees per asset. The fee amount is added on top of the quote that is generated after the ILP rate probe completes. You can define sending fees through the Backend Admin API or the [Rafiki Admin](/v1-beta/admin/admin-user-guide/#edit-asset) application.
## [Set sending fees](#set-sending-fees)
[Section titled “Set sending fees”](#set-sending-fees)
* Operation
```graphql
mutation SetFee($input: SetFeeInput!) {
setFee(input: $input) {
code
success
message
fee {
id
assetId
type
fixed
basisPoints
createdAt
}
}
}
```
* Variables
For this example, let’s assume your asset scale is 2. You’ll charge a fixed fee of 100 ($1.00) and a variable fee of 100 (1%).
```json
{
"input": {
"assetId": "14863f6f-4bda-42ef-8715-bf4762898af8",
"type": "SENDING",
"fee": {
"fixed": 100,
"basisPoints": 100
}
}
}
```
For more information about this mutation’s input object, see [`SetFeeInput`](/v1-beta/apis/graphql/backend/#definition-SetFeeInput).
* Response
```json
{
"data": {
"setFee": {
"code": "200",
"success": true,
"message": "Fee set",
"fee": {
"id": "140fd9c0-8f14-4850-9724-102f04d97e69",
"assetId": "14863f6f-4bda-42ef-8715-bf4762898af8",
"type": "SENDING",
"fixed": "100",
"basisPoints": 100,
"createdAt": "2023-09-13T14:59:53.435Z"
}
}
}
}
```
# Wallet addresses
Each payment account belonging to your users (for example, your customers) must have at least one associated wallet address for the account to be able to send and receive payments over Interledger and Open Payments. A wallet address serves as a publicly shareable standardized ID for a payment account.
Wallet address requirements
* Your Rafiki instance must be set up with at least one asset before wallet addresses can be created as each wallet address must have an asset assigned to it.
* Wallet address URLs are treated as case-insensitive, meaning that both lowercase and uppercase variations of the same address will be recognized as identical.
## [Create wallet addresses](#create-wallet-addresses)
[Section titled “Create wallet addresses”](#create-wallet-addresses)
There are a few ways in which you can create wallet addresses.
* [Through a script](#create-wallet-addresses-through-a-script)
* [In response to the `wallet_address.not_found` webhook event](#create-wallet-addresses-in-response-to-a-webhook-event)
* [In the Rafiki Admin app](/v1-beta/admin/admin-user-guide#create-wallet-address)
### [Create wallet addresses through a script](#create-wallet-addresses-through-a-script)
[Section titled “Create wallet addresses through a script”](#create-wallet-addresses-through-a-script)
Writing your own script that loops through your list of account is one way to batch create wallet addresses for your existing account holders.
Ensure your script calls the `createWalletAddress` GraphQL mutation.
* Operation
```graphql
mutation CreateWalletAddress($input: CreateWalletAddressInput!) {
createWalletAddress(input: $input) {
code
success
message
walletAddress {
id
createdAt
publicName
url
asset {
code
id
scale
}
}
}
}
```
We strongly recommend you store at least the `walletAddress.id` in your internal database to be able to reference the account and wallet address later.
* Variables
```json
{
"input": {
"assetId": "0ddc0b7d-1822-4213-948e-915dda58850b",
"publicName": "Sarah Marshall",
"url": "https://example.wallet.com/sarah",
"additionalProperties": [
{
"key": "iban",
"value": "NL93 8601 1117 947",
"visibleInOpenPayments": false
},
{
"key": "nickname",
"value": "S Mar",
"visibleInOpenPayments": true
}
]
}
}
```
For more information about this mutation’s input object, see [`CreateWalletAddressInput`](/v1-beta/apis/graphql/backend/#definition-CreateWalletAddressInput).
* Response
```json
{
"data": {
"createWalletAddress": {
"code": "200",
"success": true,
"message": "Created wallet address",
"walletAddress": {
"id": "695e7546-1803-4b45-96b6-6a53f4082018",
"createdAt": "2023-03-03T09:07:01.107Z",
"publicName": "Sarah Marshall",
"url": "https://example.wallet.com/sarah",
"asset": {
"id": "0ddc0b7d-1822-4213-948e-915dda58850b",
"code": "USD",
"scale": 2
}
}
}
}
}
```
### [Create wallet addresses in response to a webhook event](#create-wallet-addresses-in-response-to-a-webhook-event)
[Section titled “Create wallet addresses in response to a webhook event”](#create-wallet-addresses-in-response-to-a-webhook-event)
The [`wallet_address.not_found`](/v1-beta/integration/requirements/webhook-events#wallet-address-not-found) event fires when a wallet address is requested through the Open Payments Get Wallet Address API, but Rafiki can’t find the address.
When you receive the event, look up the associated account in your system, then call the `createWalletAddress` mutation to create a wallet address for the account.
The mutation and example JSON request/response is the same as what’s given [above](#create-wallet-addresses-through-a-script).
# Webhook events
The main communication channel between you and your Rafiki instance is composed of the Backend Admin API and a set of webhook events.
Most events require you to interact with Rafiki to provide wallet address information or manage (deposit or withdraw) liquidity. This page describes how you should handle each webhook event.
Note
Rafiki doesn’t hold *user* account balances. Instead, Rafiki keeps track of the liquidity within your instance’s asset, peer, and payment accounts within its own database.
## [Specify your webhook endpoint](#specify-your-webhook-endpoint)
[Section titled “Specify your webhook endpoint”](#specify-your-webhook-endpoint)
For Rafiki to notify you about webhook events, you must expose a webhook endpoint that listens for the events dispatched by Rafiki. These events notify your system of time-sensitive status updates, warnings, and errors so that you can react accordingly.
When an event occurs, the [`backend`](/v1-beta/integration/deployment/services/backend-service) service makes a POST request to your configured webhook endpoint. The `backend` service expects a `200` status in return.
| Variable | Type | Description |
| ------------- | --------- | ------------------------------------------------------------------- |
| `WEBHOOK_URL` | `backend` | The endpoint to where requests are made when a webhook event occurs |
## [Webhook event request body](#webhook-event-request-body)
[Section titled “Webhook event request body”](#webhook-event-request-body)
Each webhook event is sent as a JSON payload with the following structure in the request body. The parameters within the `data` object vary depending on the event.
| Attribute | Type | Description | Required |
| --------- | ------ | --------------------------------------------------- | -------- |
| `id` | String | UUID for the event | Y |
| `type` | Enum | The `EventType` | Y |
| `data` | Object | Additional data that coincides with the `EventType` | Y |
Duplicate events
The `id` in the webhook event payload is unique. Your system can use the ID to determine whether the event has was previously received, preventing duplicate event processing.
Expand for example JSON payloads
* incomingPaymentCompleted
```json
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"type": "incoming_payment.created",
"data": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"walletAddressId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"client": "string",
"completed": true,
"incomingAmount": "string",
"receivedAmount": "string",
"metadata": {
"additionalProp1": {}
},
"createdAt": "2024-08-29T08:13:08.966Z",
"updatedAt": "2024-08-29T08:13:08.966Z",
"expiresAt": "2024-08-29T08:13:08.966Z"
}
}
```
* outgoingPaymentCreated
```json
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"type": "outgoing_payment.created",
"data": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"walletAddressId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"client": "string",
"state": "FUNDING",
"receiver": "https://example.com/",
"debitAmount": "string",
"sentAmount": "string",
"metadata": {
"additionalProp1": {}
},
"createdAt": "2024-08-29T11:07:56.090Z",
"updatedAt": "2024-08-29T11:07:56.090Z",
"expiresAt": "2024-08-29T11:07:56.090Z",
"error": "string",
"stateAttempts": 0,
"balance": "string",
"peerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
}
```
* assetLiquidityLow
```json
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"type": "asset.liquidity_low",
"data": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"asset": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"code": "string",
"scale": 0
},
"liquidityThreshold": "string",
"balance": "string"
}
}
```
* walletAddressNotFound
```json
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"type": "wallet_address.not_found",
"data": {
"walletAddressUrl": "string"
}
}
```
We provide an [OpenAPI specification](https://github.com/interledger/rafiki/blob/main/packages/backend/src/openapi/specs/webhooks.yaml) for the webhook events fired by Rafiki.
Additionally, the [Local Playground](/v1-beta/integration/playground/overview) contains example payloads in the [Bruno collection](https://github.com/interledger/rafiki/tree/main/bruno/collections/Rafiki/Sample%20Webhook%20Events) you can use to test your webhook service integration.
## [Verify webhook signatures](#verify-webhook-signatures)
[Section titled “Verify webhook signatures”](#verify-webhook-signatures)
To protect your endpoint from unauthorized or spoofed requests, Rafiki supports an optional, but highly recommended, webhook signature verification process. By enabling signature verification, you can ensure that webhook requests are genuinely from Rafiki, and have not been tampered with.
Each webhook request includes a `Rafiki-Signature` header with a timestamp, version, and signature digest. If your instance is configured with both the `SIGNATURE_SECRET` (to generate the signature) and the `SIGNATURE_VERSION` (to set the version, defaults to v1) environment variables, you can verify the authenticity of each webhook request using the steps below.
### [Extract the timestamp and signature from the header](#extract-the-timestamp-and-signature-from-the-header)
[Section titled “Extract the timestamp and signature from the header”](#extract-the-timestamp-and-signature-from-the-header)
The `Rafiki-Signature` header in each webhook request has the following format:
Rafiki-Signature header
```bash
Rafiki-Signature: t=, v=
```
* `t=`: The UNIX timestamp (in seconds) when the signature was generated.
* `v=`: The versioned HMAC SHA-256 signature digest. The default version is `v1`.
### [Prepare the signed payload string](#prepare-the-signed-payload-string)
[Section titled “Prepare the signed payload string”](#prepare-the-signed-payload-string)
To recreate the signed payload string, concatenate the following.
* The timestamp extracted from the header
* A period (.) character
* The actual JSON payload from the request body, containing the `id`, `type`, and `data` attributes
This string format is essential for accurate signature validation.
### [Generate the expected signature](#generate-the-expected-signature)
[Section titled “Generate the expected signature”](#generate-the-expected-signature)
Use HMAC SHA-256 with the `SIGNATURE_SECRET` environment variable as the key and the signed payload string as the message.
### [Compare the signatures](#compare-the-signatures)
[Section titled “Compare the signatures”](#compare-the-signatures)
Finally, compare the signature in the header to the expected signature you generated. For security, use a constant-time comparison function to prevent timing attacks.
### [Example](#example)
[Section titled “Example”](#example)
Below is an example in JavaScript to verify Rafiki’s webhook signature:
Verify webhook signature example
```js
function verifyWebhookSignature(request: Request): boolean {
const signatureParts = request.headers['Rafiki-Signature'].split(', ')
const timestamp = signatureParts[0].split('=')[1]
const signatureVersionAndDigest = signatureParts[1].split('=')
const signatureVersion = signatureVersionAndDigest[0].replace('v', '')
const signatureDigest = signatureVersionAndDigest[1]
if (signatureVersion !== config['SIGNATURE_VERSION']) {
return false
}
const payload = `${timestamp}.${canonicalize(request.body)}`
const hmac = createHmac('sha256', config['SIGNATURE_SECRET'])
hmac.update(payload)
const digest = hmac.digest('hex')
return digest === signatureDigest
}
```
## [Event handling](#event-handling)
[Section titled “Event handling”](#event-handling)
### [Asynchronous handling](#asynchronous-handling)
[Section titled “Asynchronous handling”](#asynchronous-handling)
If requests to credit/debit user accounts are lengthy processes, we recommend using a worker to process received events. The worker allows the server to process events at a rate suitable for your system and reduces the number of failed/retried events since your event listener can immediately reply with a successful `200` status.
### [Error handling](#error-handling)
[Section titled “Error handling”](#error-handling)
If a non-200 status is returned, indicating an error, or the request times out, Rafiki retries the webhook request at increasing intervals until a `200` status is returned. The first retry occurs after 10 seconds. Additional retries occur after 20 more seconds, then after 30 more seconds, and so on.
| Variable | Type | Description |
| ------------------- | --------- | --------------------------------------------------------------------------------------------------------------- |
| `WEBHOOK_TIMEOUT` | `backend` | The amount of time, in milliseconds, after which a webhook request times out |
| `WEBHOOK_MAX_RETRY` | `backend` | The maximum number of retries for a webhook event when a non-200 status is returned or if the request timed out |
## [Webhook events](#webhook-events)
[Section titled “Webhook events”](#webhook-events)
### [Incoming payments](#incoming-payments)
[Section titled “Incoming payments”](#incoming-payments)
| Event type | Description |
| ----------------------------------------------------------- | --------------------------------------------------------------------------------- |
| [`incoming_payment.created`](#incoming-payment-created) | An incoming payment was created |
| [`incoming_payment.completed`](#incoming-payment-completed) | An incoming payment is complete and will not accept any additional incoming funds |
| [`incoming_payment.expired`](#incoming-payment-expired) | An incoming payment expired and will not accept any additional incoming funds |
#### [Incoming payment created](#incoming-payment-created)
[Section titled “Incoming payment created”](#incoming-payment-created)
Expand for event sequence
```
sequenceDiagram
participant R as Rafiki
participant ASE as Account servicing entity
R->>ASE: Fires incoming_payment.created event to webhook endpoint
ASE->>ASE: No action required
```
The `incoming_payment.created` event indicates an incoming payment was created. At this point, the incoming payment has not received any funds.
The incoming payment either completes or expires.
#### [Incoming payment completed](#incoming-payment-completed)
[Section titled “Incoming payment completed”](#incoming-payment-completed)
Expand for event sequences
Single-phase transfer An incoming payment of $10 was completed.
```
sequenceDiagram
participant R as Rafiki
participant ASE as Account servicing entity
R->>ASE: Fires incoming_payment.completed event to webhook endpoint,
receivedAmount: $10
ASE->>R: Backend Admin API call: createIncomingPaymentWithdrawal
R-->>ASE: success: true
ASE->>ASE: Credit recipient's account with $10
```
Two-phase transfer An incoming payment of $10 was completed.
```
sequenceDiagram
participant R as Rafiki
participant ASE as Account servicing entity
R->>ASE: Fires incoming_payment.completed event to webhook endpoint,
receivedAmount: $10
ASE->>R: Backend Admin API call: createIncomingPaymentWithdrawal
R-->>ASE: success: true
ASE->>ASE: Credit recipient's account with $10
ASE->>R: Backend Admin API call: postLiquidityWithdrawal
R-->>ASE: success: true
R->>R: Two-phase transfer completed
```
The `incoming_payment.completed` event indicates the payment completed either automatically or manually, and that any funds received into the incoming payment should be withdrawn and then credited to the recipient’s account on your ledger.
#### [Incoming payment expired](#incoming-payment-expired)
[Section titled “Incoming payment expired”](#incoming-payment-expired)
Expand for event sequence
2.55.
```
sequenceDiagram
participant R as Rafiki
participant ASE as Account servicing entity
R->>ASE: Fires incoming_payment.expired event to webhook endpoint,
receivedAmount: $2.55
ASE->>R: Backend Admin API call: createIncomingPaymentWithdrawal
R-->>ASE: success: true
ASE->>ASE: Credit recipient's account with $2.55
```
The `incoming_payment.expired` event will only fire if funds were received for the incoming payment. The event signals the end of any additional payments.
The primary use case for this event is to know when a streaming payment, such as one supported through [Web Monetization](https://webmonetization.org), has expired. In response to the event, any funds already received for the payment should be withdrawn and credited to the recipient’s account on your ledger.
Note
In some scenarios, a sender may not have specified an `incomingAmount` when the incoming payment was created. Receiving an `incoming_payment.expired` event indicates that no further payments are expected.
### [Outgoing payments](#outgoing-payments)
[Section titled “Outgoing payments”](#outgoing-payments)
| Event type | Description |
| ----------------------------------------------------------- | -------------------------------------------------- |
| [`outgoing_payment.created`](#outgoing-payment-created) | An outgoing payment has been created |
| [`outgoing_payment.completed`](#outgoing-payment-completed) | An outgoing payment has completed |
| [`outgoing_payment.failed`](#outgoing-payment-failed) | An outgoing payment partially or completely failed |
#### [Outgoing payment created](#outgoing-payment-created)
[Section titled “Outgoing payment created”](#outgoing-payment-created)
Expand for event sequence
An outgoing payment for $12 was created.
```
sequenceDiagram
participant R as Rafiki
participant ASE as Account servicing entity
R->>ASE: Fires outgoing_payment.created event to webhook endpoint,
debitAmount: $12
ASE->>ASE: Checks that sender's account has sufficient funds
alt Account has sufficient funds
ASE->>ASE: Put hold of $12 on sender's account
ASE->>R: Backend Admin API call: depositOutgoingPaymentLiquidity
R-->>ASE: success: true
else Account has insufficient funds
ASE->>R: Backend Admin API call: cancelOutgoingPayment,
Reason: insufficient funds
R-->>ASE: success: true
end
```
The `outgoing_payment.created` event indicates an outgoing payment was created and is awaiting liquidity. Verify the sender’s account balance and perform any other necessary verifications before funding the payment.
If the sender has insufficient funds or if the payment should otherwise not be fulfilled, cancel the outgoing payment. Otherwise, put a hold on the sender’s account and deposit the funds into Rafiki.
#### [Outgoing payment completed](#outgoing-payment-completed)
[Section titled “Outgoing payment completed”](#outgoing-payment-completed)
Expand for event sequences
Single-phase transfer An outgoing payment for 11.50 was sent. You choose to keep $0.50 as a service fee.
```
sequenceDiagram
participant R as Rafiki
participant ASE as Account servicing entity
R->>ASE: Fires outgoing_payment.completed event to webhook endpoint,
debitAmount: $12, sentAmount: $11.50
ASE->>R: Backend Admin API call: createOutgoingPaymentWithdrawal
R-->>ASE: success: true
ASE->>ASE: Remove hold and deduct $12 from sender's account,
credit your account with $0.50
```
Two-phase transfer An outgoing payment for 11.50 was sent. You choose to keep $0.50 as a service fee.
```
sequenceDiagram
participant R as Rafiki
participant ASE as Account servicing entity
R->>ASE: Fires outgoing_payment.completed event to webhook endpoint,
debitAmount: $12, sentAmount: $11.50
ASE->>R: Backend Admin API call: createOutgoingPaymentWithdrawal
R-->>ASE: success: true
ASE->>ASE: Remove hold and deduct $12 from sender's account,
credit your account with $0.50
ASE->>R: Backend Admin API call: postLiquidityWithdrawal
R-->>ASE: success: true
R->>R: Two-phase transfer complete
```
The `outgoing.payment_completed` event indicates that as much as possible has been sent to the recipient against their incoming payment.
If there is excess liquidity in Rafiki due to differences between the sent and received amounts, withdraw the excess from the outgoing payment. What you choose to do with the excess is a business decision. One option is to return the excess to the sender. Another option is to retain the excess as a service fee. Lastly, remove the hold on your sender’s account and debit their account on your ledger.
#### [Outgoing payment failed](#outgoing-payment-failed)
[Section titled “Outgoing payment failed”](#outgoing-payment-failed)
Expand for event sequence
An outgoing payment for 8 was sent successfully.
```
sequenceDiagram
participant R as Rafiki
participant ASE as Account servicing entity
R->>ASE: Fires outgoing_payment.failed event to webhook endpoint,
debitAmount: $12, sentAmount: $8
ASE->>R: Backend Admin API call: createOutgoingPaymentWithdrawal
R-->>ASE: success: true
ASE->>ASE: Remove hold and deduct $8 from the sender's account
```
The `outgoing_payment.failed` event indicates that an outgoing payment has either partially or completely failed and a retry was unsuccessful. Withdraw any remaining liquidity from the outgoing payment in Rafiki. If the payment failed completely (the `sentAmount` is `0`), remove the hold from your sender’s account. If the payment partially failed, remove the hold from your sender’s account, then debit the sender’s account on your ledger with the amount that was sent successfully. Since there will be a discrepancy between the quoted amount and the actual sent amount, we suggest you refrain from taking a sending fee.
### [Wallet addresses](#wallet-addresses)
[Section titled “Wallet addresses”](#wallet-addresses)
| Event type | Description |
| --------------------------------------------------------------------- | ------------------------------------------------------------------ |
| [`wallet_address.not_found`](#wallet-address-not-found) | The requested wallet address was not found on this Rafiki instance |
| [`wallet_address.web_monetization`](#wallet-address-web-monetization) | Web Monetization payments have been received via STREAM |
#### [Wallet address not found](#wallet-address-not-found)
[Section titled “Wallet address not found”](#wallet-address-not-found)
Expand for event sequence
The wallet address, `https://wallet.example.com/carla_garcia` was requested but does not exist.
```
sequenceDiagram
participant R as Rafiki
participant ASE as Account servicing entity
R->>ASE: Fires wallet_address.not_found event to webhook endpoint,
wallet address: https://wallet.example.com/carla_garcia
ASE->>R: Backend Admin API call: createWalletAddress,
url: https://wallet.example.com/carla_garcia,
public name: Carla Eva Garcia
R-->>ASE: success: true
```
The `wallet_address.not_found` event indicates that a wallet address was requested via the Open Payments GET wallet address API call, but the address doesn’t exist in your Rafiki instance.
When you receive this event, look up the associated account in your system and create a wallet address for the account. The initial wallet address request will succeed if you create it within your configured `WALLET_ADDRESS_LOOKUP_TIMEOUT_MS` time frame.
| Environment variable | Type | Description |
| ---------------------------------- | --------- | -------------------------------------------------------------------------------------------------------------- |
| `WALLET_ADDRESS_LOOKUP_TIMEOUT_MS` | `backend` | The time in milliseconds that you have to create a missing wallet address before the initial request times out |
#### [Wallet address Web Monetization](#wallet-address-web-monetization)
[Section titled “Wallet address Web Monetization”](#wallet-address-web-monetization)
Expand for event sequence
A wallet address received a Web Monetization payment of $0.33
```
sequenceDiagram
participant R as Rafiki
participant ASE as Account servicing entity
R->>ASE: Fires wallet_address.web_monetization event to webhook endpoint,
receivedAmount: $0.33
ASE->>R: Backend Admin API call: createWalletAddressWithdrawal
R-->>ASE: success: true
ASE->>ASE: Credit recipient's account with $0.33
```
The `wallet_address.web_monetization` event indicates that a wallet address received Web Monetization payments via the ILP STREAM protocol. Withdraw the liquidity from the wallet address in Rafiki and credit the recipient’s account on your ledger.
### [Low asset liquidity](#low-asset-liquidity)
[Section titled “Low asset liquidity”](#low-asset-liquidity)
| Event type | Description |
| --------------------------------------------- | ------------------------------------------------------------- |
| [`asset.liquidity_low`](#asset-liquidity-low) | Your asset liquidity has dropped below your defined threshold |
#### [Asset liquidity low](#asset-liquidity-low)
[Section titled “Asset liquidity low”](#asset-liquidity-low)
Expand for event sequence
Your asset liquidity for USD (asset scale: 2) drops below $100.00.
```
sequenceDiagram
participant R as Rafiki
participant ASE as Account servicing entity
R->>ASE: Fires asset.liquidity_low event to webhook endpoint,
asset: USD (scale: 2, id: "abc")
ASE->>R: Backend Admin API call: depositAssetLiquidity
R-->>ASE: success: true
```
The `asset.liquidity_low` event indicates that an asset’s liquidity has dropped below your predefined liquidity threshold. Check if you already have, or can acquire, additional liquidity for that specific asset. If so, deposit it in Rafiki. Cross-currency transfers will fail if you don’t increase the asset’s liquidity.
### [Low peer liquidity](#low-peer-liquidity)
[Section titled “Low peer liquidity”](#low-peer-liquidity)
| Event type | Description |
| ------------------------------------------- | ------------------------------------------------------------ |
| [`peer.liquidity_low`](#peer-liquidity-low) | Your peer liquidity has dropped below your defined threshold |
#### [Peer liquidity low](#peer-liquidity-low)
[Section titled “Peer liquidity low”](#peer-liquidity-low)
Expand for event sequence
The liquidity for your peer, Happy Life Bank, drops below $100.00 USD.
```
sequenceDiagram
participant R as Rafiki
participant ASE as Account servicing entity
R->>ASE: Fires peer.liquidity_low event to webhook endpoint,
peer: Happy Life Bank (asset: "USD", scale: 2, id: "abc")
ASE->>R: Backend Admin API call: depositPeerLiquidity
R-->>ASE: success: true
```
The `peer.liquidity_low` event indicates that a peer’s liquidity has dropped below your predefined liquidity threshold. Decide whether you want to extend the peer’s credit line or if your peer must settle before you will extend a new line of credit. If you cannot or do not increase the peer liquidity in Rafiki, transfers to that peer will fail.
# Account servicing entity (ASE)
An account servicing entity (ASE) is a regulated entity that provides and maintains payment accounts for its customers. Examples of ASEs include banks, digital wallet providers, and mobile money providers.
As regulated entities, ASEs are subject to the laws, rules, and regulations of their jurisdictions. As such, Rafiki should **not** be used in production environments by non-regulated entities.
## [Responsibilities and obligations](#responsibilities-and-obligations)
[Section titled “Responsibilities and obligations”](#responsibilities-and-obligations)
A few examples of an ASE’s responsibilities and obligations include:
* Regulatory compliance
* Account provisioning and maintenance
* Transaction handling
* Ledger management
* Authentication and consent
### [Regulatory compliance](#regulatory-compliance)
[Section titled “Regulatory compliance”](#regulatory-compliance)
ASEs must onboard account holders in compliance with regulatory requirements, such as performing Know Your Customer (KYC) checks, anti-money laundering (AML) processes, and sanctions screening.
### [Account provisioning and maintenance](#account-provisioning-and-maintenance)
[Section titled “Account provisioning and maintenance”](#account-provisioning-and-maintenance)
ASEs manage the creation, upkeep, and security of payment accounts. This includes providing channels for account holders (individuals or businesses) to interact with their accounts via mobile apps, websites, and other interfaces.
### [Transaction handling](#transaction-handling)
[Section titled “Transaction handling”](#transaction-handling)
ASEs handle deposits and withdrawals through various external payment methods such as bank transfers, credit cards, and other payment services.
### [Ledger management](#ledger-management)
[Section titled “Ledger management”](#ledger-management)
ASEs maintain a ledger of account balances and transaction histories for their account holders.
### [Authentication and consent](#authentication-and-consent)
[Section titled “Authentication and consent”](#authentication-and-consent)
In the context of Open Payments, ASEs are responsible for authenticating resource owners (for example, account holders) and obtaining their consent when clients, such as mobile apps, request access to a resource (for example, an account).
# Accounting in Rafiki
Rafiki uses [double-entry accounting](https://en.wikipedia.org/wiki/Double-entry_bookkeeping) to record financial transactions. In this method of bookkeeping, a transaction recorded to one account results in an equal and opposite entry to another account. For example, a $50 credit to one account results in a $50 debit from another account.
Transactions in Rafiki represent Interledger packet interactions, denominated in a given [asset](#assets). Packet interactions can be successful, fail, or be rejected. Rafiki’s accounting layer processes the interactions and converts the activities into financial records, which are then written to your [accounting database](#accounting-databases).
Accounts within Rafiki are your internal [liquidity](#liquidity-accounts) and [settlement](#settlement-accounts) accounts used to fund payments, not the accounts that you service for your customers. This distinction is crucial for understanding how Rafiki handles transactions and settlements.
## [Assets](#assets)
[Section titled “Assets”](#assets)
An asset represents a transferrable item of value. Although the Interledger Protocol (ILP) supports the transfer of any asset deemed to have value, assets are generally denominated in a currency. For example fiat currencies, central bank digital currencies, and branded currencies (such as merchant reward points).
Part of Rafiki’s [integration requirements](/v1-beta/integration/requirements/assets) include adding one or more assets that you support.
An asset is made up of the following properties.
| Property | Type | Description | Example |
| ------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------ | ------- |
| `value` | BigInt | A numerical amount | `10000` |
| `assetCode` | String | A code representing the asset. An [ISO 4217 currency code](https://en.wikipedia.org/wiki/ISO_4217) should be used whenever possible. | `"USD"` |
| `assetScale` | Integer | Difference in order of magnitude between the standard unit and a fractional unit | `2` |
To convert an asset’s value into an amount that’s easier to interpret, apply the following formula.
\= *currencyAmount*
Using the example data from the preceding table, the formula looks like this:
100.00 USD
## [Accounts](#accounts)
[Section titled “Accounts”](#accounts)
Rafiki uses a combination of liquidity and settlement accounts to track the amounts available to fund transactions. Rafiki does not physically hold funds in each account. Instead, it uses [double-entry accounting](https://en.wikipedia.org/wiki/Double-entry_bookkeeping) to record the transactions. The actual settlement of amounts owed, in which funds are physically exchanged, occurs outside of both Rafiki and the Interledger Protocol.
### [Liquidity accounts](#liquidity-accounts)
[Section titled “Liquidity accounts”](#liquidity-accounts)
Liquidity accounts track deposits, withdrawals, and transfers that occur during the course of a transaction. Rafiki provides liquidity accounts for assets, peers, and payments.
Liquidity accounts hold either a zero or a positive balance. Rafiki ensures that the total debits to a liquidity account will not exceed the account’s total credits.
| Account type | What the account represents | Number of accounts |
| ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- | ------------------------ |
| [Asset liquidity](#asset-liquidity-accounts) | The value, denominated in a given asset, that Rafiki has available to support cross-currency transactions | One per asset |
| [Peer liquidity](#peer-liquidity-accounts) | The credit line, denominated in the asset of your peering relationship, that you extend to a peer | One per peer |
| [Incoming payment liquidity](#incoming-payment-liquidity-accounts) | The value received from a completed incoming payment | One per incoming payment |
| [Outgoing payment liquidity](#outgoing-payment-liquidity-accounts) | The value that Rafiki will attempt to send in an outgoing payment | One per outgoing payment |
| [Wallet address liquidity](#wallet-address-liquidity-accounts) | The value that a wallet address received via SPSP | One per wallet address |
#### [Asset liquidity accounts](#asset-liquidity-accounts)
[Section titled “Asset liquidity accounts”](#asset-liquidity-accounts)
Asset liquidity ensures Rafiki has enough liquidity, denominated in a given asset, to handle cross-currency (foreign exchange) transactions.
An asset liquidity account represents the value that Rafiki has available for sending or forwarding ILP packets. You have one asset liquidity account for each asset you transact in.
Whenever an outgoing payment/incoming payment is in a different asset than the peering relationship, the liquidity of asset accounts change depending on the FX direction. Any transaction that would result in a negative balance will fail.
Note
If you and your peer transact in the same asset (there’s no currency conversion) and you both provide your customers only with wallet addresses denominated in that asset, then there will be no movement into/from the corresponding asset’s liquidity account.
For example, you and your peer transact in USD and only provide your customers with USD wallet addresses. One of your customers sends $10 to your peer’s customer. There’s no movement from your USD asset liquidity account because there was no currency conversion. There is, however, an [outgoing payment liquidity account](/v1-beta/overview/concepts/accounting#outgoing-payment-liquidity-accounts) created to support the transaction.
You can add a liquidity threshold for each asset liquidity account via the [`updateAsset`](https://rafiki.dev/apis/graphql/backend/mutations/#updateasset) mutation’s `liquidityThreshold` input argument.
When a threshold is entered, the [`asset.liquidity_low`](/v1-beta/integration/requirements/webhook-events#asset-liquidity-low) webhook event notifies you if an asset account’s liquidity drops below the threshold.
You should define and adjust asset liquidity based on your liquidity risk profile. You can deposit or withdraw asset liquidity as needed through [Rafiki Admin](/v1-beta/admin/admin-user-guide#edit-asset) or by using the [Backend Admin API](/v1-beta/admin/liquidity/asset-liquidity#manage-asset-liquidity-using-the-backend-admin-api).
Asset liquidity example - cross-currency transactions
Your Rafiki instance is configured for two assets: EUR and USD.
* Rafiki holds an asset liquidity account for both EUR and USD.
* You’ve set the asset scale of both currencies to 0.
* Your starting EUR liquidity is 10 and your USD liquidity is 50.
**Cross-currency transaction #1:**
1. Rafiki receives packets from a peer. These packets are all denominated in EUR, worth €10. 10 EUR move from the peer’s liquidity account on your Rafiki instance to your EUR asset liquidity account. Your EUR liquidity increases to 20 (10 + 10).
2. The EUR-to-USD exchange rate is applied, with €10 equating to $12 USD. Since your starting USD liquidity is 50, your USD asset liquidity account can cover the transfer of $12 USD to an incoming payment liquidity account. Your USD liquidity decreases to 38 (50 - 12).
**Cross-currency transaction #2:**
1. Rafiki receives packets from a peer. These packets are all denominated in EUR, worth €50. Your EUR liquidity increases to 70 (20 + 50).
2. The current EUR-to-USD exchange rate is applied, with €50 equating to $55 USD. The transaction fails. Your USD liquidity account is 38, so you don’t have enough liquidity to cover the transaction.
3. Your EUR liquidity reduces back to 20 (70 - 50).
#### [Peer liquidity accounts](#peer-liquidity-accounts)
[Section titled “Peer liquidity accounts”](#peer-liquidity-accounts)
Peer liquidity is the credit line you’ve extended to a peer. A peer liquidity account represents the amount of the line of credit that the peer still has available to them. You have one liquidity account for each peer and the account is denominated in the asset you both agreed to transact in.
The amount of credit that you extend to a peer, the asset that you transact in, and the mechanism you use to settle are just a few items that should be defined in your respective peering agreements.
Note
A peering agreement is a legal contract between the parties involved in a peering relationship. It defines terms such as the assets involved and other operational details. It is not configured or managed within Rafiki but is necessary for establishing the terms under which assets are exchanged.
If a peer’s liquidity is insufficient (for example, they’ve used up their allotted credit line), payments will not be processed. Your peer should settle with you so that you can reset their liquidity.
You can add a liquidity threshold for each peer liquidity account via the [`updatePeer`](https://rafiki.dev/apis/graphql/backend/mutations/#updatepeer) mutation’s `liquidityThreshold` input argument.
When a threshold is entered, the [`peer.liquidity_low`](/v1-beta/integration/requirements/webhook-events#peer-liquidity-low) webhook event notifies you if a peer’s liquidity drops below the threshold.
You should define and adjust each peer’s liquidity based on your liquidity risk profile. You can deposit or withdraw peer liquidity as needed through [Rafiki Admin](/v1-beta/admin/admin-user-guide#edit-peer) or by using the [Backend Admin API](/v1-beta/admin/liquidity/peer-liquidity#manage-peer-liquidity-using-the-backend-admin-api).
Peer liquidity example
You and Cloud Nine Wallet are peers. You’ve agreed to extend Cloud Nine Wallet a line of credit worth $100.00 USD. This means Cloud Nine Wallet has $100.00 in their peer liquidity account on your Rafiki instance. Your Rafiki instance can receive packets that total up to $100.00 from Cloud Nine Wallet. When the $100.00 is used up, Cloud Nine Wallet settles with you by sending $100.00 via the shared settlement mechanism outlined in your peering agreement. When you receive the funds, you reset their liquidity in Rafiki.
#### [Payment liquidity accounts](#payment-liquidity-accounts)
[Section titled “Payment liquidity accounts”](#payment-liquidity-accounts)
Payment liquidity is the amount that’s available because of an incoming or outgoing payment. Rafiki has three types of payment liquidity accounts.
| Payment type | Purpose |
| ---------------------------------------------------- | ------------------------------------------------------- |
| [Incoming](#incoming-payment-liquidity-accounts) | For incoming payments created via the Open Payments API |
| [Outgoing](#outgoing-payment-liquidity-accounts) | For outgoing payments created via the Open Payments API |
| [Wallet address](#wallet-address-liquidity-accounts) | For payments sent via SPSP |
##### [Incoming payment liquidity accounts](#incoming-payment-liquidity-accounts)
[Section titled “Incoming payment liquidity accounts”](#incoming-payment-liquidity-accounts)
An incoming payment liquidity account represents the value received for an incoming payment. Incoming payments are created via [Open Payments](https://openpayments.dev/apis/resource-server/operations/create-incoming-payment/). When the first packet for an incoming payment is received, a corresponding liquidity account is automatically created. You will have one liquidity account per incoming payment.
You are notified of created, completed, and expired incoming payments by listening for the appropriate [webhook events](/v1-beta/integration/requirements/webhook-events/#incoming-payments). Since Rafiki doesn’t hold funds, anything you receive must be withdrawn and then credited to the recipient’s account on your ledger.
The liquidity account isn’t used again after the payment completes, but its record remains in your accounting database. When a new incoming payment occurs, a new liquidity account is created.
##### [Outgoing payment liquidity accounts](#outgoing-payment-liquidity-accounts)
[Section titled “Outgoing payment liquidity accounts”](#outgoing-payment-liquidity-accounts)
An outgoing payment liquidity account represents the value available to send in an outgoing payment. When an outgoing payment is created via [Open Payments](https://openpayments.dev/apis/resource-server/operations/create-outgoing-payment/), a corresponding liquidity account is automatically created. You will have one liquidity account per outgoing payment.
You are notified of created, completed, and failed outgoing payments by listening for the appropriate [webhook events](/v1-beta/integration/requirements/webhook-events/#outgoing-payments). Liquidity must be deposited into the outgoing payment account before the payment can be processed.
You may occasionally have excess liquidity, such as when an outgoing payment only partially completes and a portion of the send-amount remains. Since Rafiki doesn’t hold funds, any excess liquidity that remains after an outgoing payment completes must be withdrawn from the outgoing payment liquidity account. How you choose to handle the excess is up to you. You could, for example, refund the excess to the sender or take the amount as a fee.
The account isn’t used again after the payment completes, but its record remains in your accounting database. When a new outgoing payment is created, a new liquidity account is created.
##### [Wallet address liquidity accounts](#wallet-address-liquidity-accounts)
[Section titled “Wallet address liquidity accounts”](#wallet-address-liquidity-accounts)
A wallet address liquidity account contains the value received to a wallet address via [SPSP](/v1-beta/overview/concepts/interledger#simple-payment-setup-protocol-spsp). When an incoming payment is created, a corresponding liquidity account is automatically created. You will have one account per wallet address.
Since Rafiki doesn’t hold funds, you must withdraw the liquidity when the payment completes and credit the funds to the recipient’s account on your ledger. You are notified to withdraw liquidity by listening for the appropriate [webhook event](/v1-beta/integration/requirements/webhook-events#wallet-addresses).
Unlike the incoming and outgoing payment liquidity accounts, the same wallet address liquidity account will be used for future incoming SPSP payments.
### [Settlement accounts](#settlement-accounts)
[Section titled “Settlement accounts”](#settlement-accounts)
A settlement account represents the total funds, denominated in a single asset, that you have deposited into Rafiki. You have one settlement account for each asset you transact in.
Settlement accounts hold either a zero or a negative balance. A negative balance on a settlement account means you’ve deposited more funds into Rafiki than you’ve withdrawn. The closer a settlement account’s balance is to 0, the more likely it is you need to settle with your peer for the amount owed and then deposit the amount back into Rafiki.
Rafiki ensures that the total credits to a settlement account do not exceed its total debits.
Settlement account example
You deposit $10,000 into a peer’s liquidity account, meaning you’ve extended a credit line of $10,000 to your peer.
Your peer liquidity account balance is $10,000 and your USD settlement account balance is now -$10,000.
An incoming payment from your peer for $100 is created, meaning your peer is using $100 of their line of credit. Since Rafiki doesn’t hold funds, you must withdraw the liquidity and credit the amount to the recipient’s account on your ledger.
Now, your peer liquidity account’s balance is $9,900 and your USD settlement account’s balance is -$9,900.
## [Accounting databases](#accounting-databases)
[Section titled “Accounting databases”](#accounting-databases)
### [TigerBeetle](#tigerbeetle)
[Section titled “TigerBeetle”](#tigerbeetle)
TigerBeetle is a high-performance distributed financial accounting database used by Rafiki’s [backend service](/v1-beta/integration/deployment/services/backend-service) to store account balance data. Both liquidity and settlement accounts in Rafiki correspond to TigerBeetle credit and debit accounts, respectively.
TigerBeetle only holds balance data without any additional ILP packet metadata. For detailed information on TigerBeetle, including its consensus mechanism and its limitations, visit the official TigerBeetle [documentation](https://docs.tigerbeetle.com/) and [blog](https://tigerbeetle.com/blog/). For more information about TigerBeetle in a production Rafiki environment, see [Running Rafiki in production](/v1-beta/integration/deployment/helm-k8s).
### [Postgres](#postgres)
[Section titled “Postgres”](#postgres)
You can choose to use a separate Postgres database for accounting instead of using TigerBeetle. However, TigerBeetle is recommended due to its speed, efficiency, and dedicated design for handling double-entry/double-ledger accounting.
## [Transfers](#transfers)
[Section titled “Transfers”](#transfers)
As with the accounts described above, Rafiki performs [double-entry accounting](https://en.wikipedia.org/wiki/Double-entry_bookkeeping) for transfers, where increasing the total debits of one account increases the total credits of another account by the same amount, and vice versa.
Transfers can complete in either a single phase or in two phases.
### [Single-phase transfer](#single-phase-transfer)
[Section titled “Single-phase transfer”](#single-phase-transfer)
A single-phase transfer posts funds to accounts immediately when the transfer is created.
**Example of successful single-phase incoming payment**
```
sequenceDiagram
participant R as Rafiki
participant ASE as Account servicing entity
R->>ASE: Fires webhook event when incoming payment completes
ASE->>R: Withdraws payment amount from incoming payment liquidity account
ASE->>ASE: Credits the recipient's account by the payment amount
```
### [Two-phase transfer](#two-phase-transfer)
[Section titled “Two-phase transfer”](#two-phase-transfer)
A two-phase transfer moves funds in two stages.
1. Reserve funds (`pending`)
2. Resolve funds (`post`, `void`, or `expire`)
**Example of successful two-phase incoming payment**
```
sequenceDiagram
Rafiki->>ASE: Fires webhook event when incoming payment completes
ASE->>Rafiki: Withdraws payment amount from incoming payment
liquidity account (reserve funds pending)
ASE->>ASE: Credits the recipient's account by the payment amount
ASE->>Rafiki: Resolve funds (post)
Rafiki->>Rafiki: Two-phase transfer complete
```
The name two-phase transfer is a reference to the [two-phase commit protocol](https://en.wikipedia.org/wiki/Two-phase_commit_protocol) for distributed transactions.
You can [post and commit](/v1-beta/admin/liquidity/two-phase-transfers#post-and-commit-a-successful-transfer) a successful two-phase transfer and [void and roll back](/v1-beta/admin/liquidity/two-phase-transfers#void-and-roll-back-an-unsuccessful-transfer) an unsuccessful two-phase transfer by using the Backend Admin API.
### [Intra-Rafiki transfer examples](#intra-rafiki-transfer-examples)
[Section titled “Intra-Rafiki transfer examples”](#intra-rafiki-transfer-examples)
Remember that a settlement account will always have a zero or negative balance and a liquidity account will always have a zero or positive balance.
* [Deposits](#deposits)
* [Withdrawals](#withdrawals)
* [Payments in the same asset](#payments-in-the-same-asset)
* [Cross currency payments](#cross-currency-payments)
#### [Deposits](#deposits)
[Section titled “Deposits”](#deposits)
A deposit is the act of debiting the settlement account and crediting the liquidity account.
**Example:** Depositing `100 USD` asset liquidity
| Debit Account | Credit Account |
| ------------- | --------------- |
| Settlement | Asset liquidity |
| USD settlement acct | USD asset liquidity acct |
| --------------------- | ------------------------ |
| Debit Credit
100 | Debit Credit
100 |
**Example:** Depositing `100 USD` peer liquidity
| Debit Account | Credit Account |
| ------------- | -------------- |
| Settlement | Peer liquidity |
| USD settlement acct | USD peer liquidity acct |
| --------------------- | ----------------------- |
| Debit Credit
100 | Debit Credit
100 |
**Example:** Depositing `35 USD` outgoing payment liquidity
| Debit Account | Credit Account |
| ------------- | ---------------- |
| Settlement | Outgoing payment |
| USD settlement acct | USD outgoing payment liquidity acct |
| -------------------- | ----------------------------------- |
| Debit Credit
35 | Debit Credit
35 |
#### [Withdrawals](#withdrawals)
[Section titled “Withdrawals”](#withdrawals)
A withdrawal is the act of debiting the liquidity account and crediting the settlement account.
**Example:** Withdrawing `50 USD` in asset liquidity
| Debit Account | Credit Account |
| --------------- | -------------- |
| Asset liquidity | Settlement |
| USD asset liquidity acct | USD settlement acct |
| ------------------------ | ------------------- |
| Debit Credit
50 | Debit Credit
50 |
**Example:** Withdrawing `50 USD` in peer liquidity
| Debit Account | Credit Account |
| -------------- | -------------- |
| Peer liquidity | Settlement |
| USD peer liquidity acct | USD settlement acct |
| ----------------------- | ------------------- |
| Debit Credit
50 | Debit Credit
50 |
**Example:** Withdrawing `2 USD` in wallet address liquidity
| Debit Account | Credit Account |
| -------------- | -------------- |
| Wallet address | Settlement |
| USD wallet address liquidity acct | USD settlement acct |
| --------------------------------- | ------------------- |
| Debit Credit
2 | Debit Credit
2 |
**Example:** Withdrawing `2 USD` in incoming payment liquidity
| Debit Account | Credit Account |
| ---------------- | -------------- |
| Incoming payment | Settlement |
| USD incoming payment liquidity acct | USD settlement acct |
| ----------------------------------- | ------------------- |
| Debit Credit
25 | Debit Credit
25 |
**Example:** Withdrawing `1 USD` in outgoing payment liquidity
| Debit Account | Credit Account |
| ---------------- | -------------- |
| Outgoing payment | Settlement |
| USD outgoing payment liquidity acct | USD settlement acct |
| ----------------------------------- | ------------------- |
| Debit Credit
1 | Debit Credit
1 |
#### [Payments in the same asset](#payments-in-the-same-asset)
[Section titled “Payments in the same asset”](#payments-in-the-same-asset)
**Example:** Sender consented to a payment of `14 USD` but the quote promised to deliver `15 USD`. The send amount is less than the receive amount.
| Debit Account | Credit Account |
| ---------------- | ---------------- |
| Outgoing payment | Incoming payment |
| Asset liquidity | Incoming payment |
| USD outgoing payment liquidity acct | USD asset liquidity acct | USD incoming payment liquidity acct |
| ----------------------------------- | ------------------------ | ----------------------------------- |
| Debit Credit
14 | Debit Credit
1 | Debit Credit
15 |
**Example:** Sender consented to a payment of `15 USD` but the quote promised to deliver `14 USD`. The send amount is more than the receive amount.
| Debit Account | Credit Account |
| ---------------- | ---------------- |
| Outgoing payment | Incoming payment |
| Outgoing payment | Asset liquidity |
| USD outgoing payment liquidity acct | USD asset liquidity acct | USD incoming payment liquidity acct |
| ----------------------------------- | ------------------------ | ----------------------------------- |
| Debit Credit
15 | Debit Credit
1 | Debit Credit
14 |
#### [Cross currency payments](#cross-currency-payments)
[Section titled “Cross currency payments”](#cross-currency-payments)
**Example:** Outgoing payment is for `10 USD`, incoming payment receives `9 EUR` after a currency exchange
| Debit Account | Credit Account | Asset |
| ---------------- | ---------------- | ----- |
| Outgoing payment | Asset liquidity | `USD` |
| Asset liquidity | Incoming payment | `EUR` |
| USD outgoing payment liquidity acct | USD asset liquidity acct |
| ----------------------------------- | ----------------------------------- |
| Debit Credit
10 | Debit Credit
10 |
| EUR asset liquidity acct | EUR incoming payment liquidity acct |
| Debit Credit
9 | Debit Credit
9 |
### [Interledger transfer examples](#interledger-transfer-examples)
[Section titled “Interledger transfer examples”](#interledger-transfer-examples)
In these examples, the sender and receiver do not have wallet addresses at the same Rafiki instance.
Remember that a settlement account will always have a zero or negative balance and a liquidity account will always have a zero or positive balance.
* [Sending connector - same asset](#sending-connector---same-asset)
* [Sending connector - cross currency](#sending-connector---cross-currency)
* [Receiving connector - same asset](#receiving-connector---same-asset)
* [Receiving connector - cross currency](#receiving-connector---cross-currency)
* [Connector - same asset](#connector---same-asset)
* [Connector - cross currency](#connector---cross-currency)
#### [Sending connector - same asset](#sending-connector---same-asset)
[Section titled “Sending connector - same asset”](#sending-connector---same-asset)
**Example:** Sender creates an outgoing payment for `100 USD` to an incoming payment in the same asset at a peer’s Rafiki instance
| Debit Account | Credit Account |
| ---------------- | -------------- |
| Outgoing payment | Peer liquidity |
| USD outgoing payment liquidity acct | USD peer liquidity acct |
| ----------------------------------- | ----------------------- |
| Debit Credit
100 | Debit Credit
100 |
#### [Sending connector - cross currency](#sending-connector---cross-currency)
[Section titled “Sending connector - cross currency”](#sending-connector---cross-currency)
**Example:** Sender creates an outgoing payment for `100 USD` to an incoming payment at a peer’s Rafiki instance. The peering relationship is in EUR, so the payment is converted on the sending side.
| Debit Account | Credit Account | Asset |
| ---------------- | --------------- | ----- |
| Outgoing payment | Asset liquidity | `USD` |
| Asset Liquidity | Peer Liquidity | `EUR` |
| USD outgoing payment liquidity acct | USD asset liquidity acct |
| ----------------------------------- | ------------------------ |
| Debit Credit
100 | Debit Credit
100 |
| EUR asset liquidity acct | EUR peer liquidity acct |
| Debit Credit
90 | Debit Credit
90 |
#### [Receiving connector - same asset](#receiving-connector---same-asset)
[Section titled “Receiving connector - same asset”](#receiving-connector---same-asset)
**Example:** An incoming payment receives `100 USD` from an outgoing payment in the same asset at a peer’s Rafiki instance.
| Debit Account | Credit Account |
| -------------- | ---------------- |
| Peer liquidity | Incoming payment |
| USD peer liquidity acct | USD incoming payment liquidity acct |
| ----------------------- | ----------------------------------- |
| Debit Credit
100 | Debit Credit
100 |
#### [Receiving connector - cross currency](#receiving-connector---cross-currency)
[Section titled “Receiving connector - cross currency”](#receiving-connector---cross-currency)
**Example:** A Rafiki instance receives `10 USD` from a peer (peering relationship in USD) to be deposited in an incoming payment liquidity account denominated in EUR. The payment is converted to EUR and deposited.
| Debit Account | Credit Account | Asset |
| --------------- | ---------------- | ----- |
| Peer liquidity | Asset liquidity | `USD` |
| Asset liquidity | Incoming payment | `EUR` |
| USD peer liquidity acct | USD asset liquidity acct |
| ------------------------ | ----------------------------------- |
| Debit Credit
10 | Debit Credit
10 |
| EUR asset liquidity acct | EUR incoming payment liquidity acct |
| Debit Credit
9 | Debit Credit
9 |
#### [Connector - same asset](#connector---same-asset)
[Section titled “Connector - same asset”](#connector---same-asset)
**Example:** Rafiki forwards `10 USD` from peer A to peer B.
| Debit Account | Credit Account |
| -------------- | -------------- |
| Peer liquidity | Peer liquidity |
| USD peer “A” liquidity acct | USD peer “B” liquidity acct |
| --------------------------- | --------------------------- |
| Debit Credit
10 | Debit Credit
10 |
#### [Connector - cross currency](#connector---cross-currency)
[Section titled “Connector - cross currency”](#connector---cross-currency)
**Example:** Rafiki receives `100 USD` from peer A and forwards `90 EUR` to peer B.
| Debit Account | Credit Account | Asset |
| --------------- | --------------- | ----- |
| Peer liquidity | Asset liquidity | `USD` |
| Asset liquidity | Peer liquidity | `EUR` |
| USD peer “A” liquidity acct | USD asset liquidity acct |
| --------------------------- | --------------------------- |
| Debit Credit
100 | Debit Credit
100 |
| EUR asset liquidity acct | EUR peer “B” liquidity acct |
| Debit Credit
90 | Debit Credit
90 |
# Clearing and settlement
## [Clearing](#clearing)
[Section titled “Clearing”](#clearing)
When a payment is made over traditional banking rails, the money doesn’t move instantly. First, there are checks to confirm that the money exists and can be transferred. Clearing networks are responsible for exchanging messages between ASEs to facilitate these checks. This process is called clearing. When a payment successfully clears, it means the payer’s ASE has an obligation to the payee’s ASE.
The [Interledger Protocol (ILP)](/v1-beta/overview/concepts/interledger) is not a traditional clearing network, but does function in a similar way.
* ASEs that implement the protocol must become [peers](/v1-beta/integration/requirements/peers) to transact with one another. This is comparable to traditional banking, where ASEs must use the same clearing network. An ASE can’t use Interledger to transact with another ASE unless they have both implemented the protocol and have peered with one another.
* Peered ASEs exchange ILP packets, which are packets of value that contain transaction information. ILP packets are akin to the messages exchanged during the traditional clearing process.
* The successful exchange of ILP packets between peers creates obligations between them that must be settled. The receipt of a fulfilled ILP packet is basically a conditional IOU—a promise to pay—that affects the financial accounting balances between the peers.
You can read more about clearing as it relates to ILP in the [Interledger developer docs](https://interledger.org/developers/rfcs/peering-clearing-settling/).
Conceptually, Rafiki sits at the clearing level, but isn’t a clearing network. It’s software that makes implementing the Interledger protocol faster and easier. Rafiki uses ILP to [track liquidity](/v1-beta/overview/concepts/accounting) between assets, payments, and peers. An ASE must still connect Rafiki to their existing backend system and internal ledger for authentication, fetching exchange rates, and managing liquidity itself. For example, if an incoming payment completes in Rafiki, the ASE’s backend must credit the recipient’s account on their own system, however that might look.
In any case, no movement of actual money has occurred yet.
## [Settlement](#settlement)
[Section titled “Settlement”](#settlement)
In traditional banking, settlement is the fulfillment of an obligation between ASEs. It turns the promise of payment into a real payment by moving actual money. This occurs over a shared settlement network, such as Fedwire in the United States.
When a payer’s ASE settles with the payee’s ASE, there’s a high chance that the ASE isn’t physically handing over cash. There’s more likely to be an intermediary, like a reserve bank or central bank, that maintains accounts for both ASEs. The intermediary moves funds from one account to the other, crediting and debiting the accounts as necessary.
With Interledger, the concept of settlement is not that different. Each [peer](/v1-beta/integration/requirements/peers) must agree on a settlement system to use to fulfill their obligations with one another. However, ILP itself is not a settlement system. This means peers must have some other way to fulfill their obligations and exchange value. Examples can include using a real-time gross settlement system like Fedwire, an automated clearing house (ACH) network, a money transfer service, or some other payment channel. You can read more about settling as it relates to ILP in the [Interledger developer docs](https://interledger.org/developers/rfcs/settlement-engines/).
Rafiki is also not a settlement system. It [keeps track of liquidity](/v1-beta/overview/concepts/accounting) through the use of liquidity and settlement accounts. Liquidity accounts track deposit, withdrawal, and transfer amounts. Settlement accounts track the availability of funds, denominated in a single asset. A negative balance means funds are available. The closer a settlement account’s balance is to zero, the more likely it is that one peer needs to settle with the other over their agreed-upon settlement system.
# Interledger
Building and maintaining your own connector to participate on the Interledger network can be a time consuming and complex undertaking. As a reference implementation of the Interledger stack, Rafiki gives you all the tools you need to quickly and easily join the network and enable Interledger capabilities on your users’ accounts.
## [Packets](#packets)
[Section titled “Packets”](#packets)
At the core of Interledger is the Interledger Protocol (ILP). It’s a request/response protocol where requests and responses are ILP packets.
These packets of data carry information about a payment. Typically, information about a single aggregate payment from sender to receiver is split into multiple ILP packets.
Each packet represents a conditional IOU which affects financial accounting balances between peers. Amounts adjust based on the sender’s asset, the receiver’s asset, and Rafiki’s configured [exchange rates service](/v1-beta/integration/requirements/exchange-rates). Then, the amounts are used to update account liquidity in your accounting database (TigerBeetle or Postgres).
## [Peers](#peers)
[Section titled “Peers”](#peers)
Interledger itself is a network of computers that enables sending payment messages across payment networks. Each computer on the network is a node.
For two nodes on the Interledger network to exchange ILP packets with one another, the two nodes must be peers. There are a number of [requirements](/v1-beta/integration/requirements/peers#perform-prerequisites) that both you and your potential peer must meet to form a peering relationship.
Since the purpose of peering is to facilitate payments, which often involves extending lines of credit, your peer should be someone you trust. We strongly recommend you and your potential peer define your expectations and outline your agreements in a legally binding document peering with one another.
## [Connectors](#connectors)
[Section titled “Connectors”](#connectors)
Each node on the Interledger network can take on the role of sender, connector, or receiver, depending on the payment.
* Sender - Originates the payment by sending ILP packets.
* Connector - An intermediary between a sender and receiver that forwards ILP packets. Connectors can facilitate payments to or from anyone they’re peered with.
* Receiver - The final recipient of the ILP packets and, as such, the payment.
If the sender and receiver nodes are peers, then the payment flow is straightforward and no intermediary connector nodes are needed. However, if the sender and receiver aren’t peers, then the payment must route through one or more connectors. Rafiki’s [backend service](/v1-beta/integration/deployment/services/backend-service#interledger-connector) includes an Interledger connector for sending and receiving ILP packets.
In the image below, the sender node (A) and the receiver node (C) share a common peer (B). In payments from the sender to the receiver, node B performs the role of connector to facilitate payments between the two.

In reality, there can be multiple connectors between a sender node and a receiver node. As more nodes that support different assets peer with one another, the easier it becomes for payments to traverse the Interledger network.
## [Payment pointer](#payment-pointer)
[Section titled “Payment pointer”](#payment-pointer)
A payment pointer is a type of wallet address that serves as an SPSP endpoint to facilitate sending and receiving ILP packets. Rafiki will assign each of your customers’ accounts with a payment pointer.
Payment pointers must resolve to an HTTPS URL and can be written out using the `$` shorthand (for example, `$wallet.example.com/alice`) or as a URL (for example, `https://wallet.example.com/alice`).
See [Payment pointers and wallet addresses](/v1-beta/overview/concepts/payment-pointers) for more information.
## [Simple Payment Setup Protocol (SPSP)](#simple-payment-setup-protocol-spsp)
[Section titled “Simple Payment Setup Protocol (SPSP)”](#simple-payment-setup-protocol-spsp)
The [Simple Payment Setup Protocol (SPSP)](https://interledger.org/developers/rfcs/simple-payment-setup-protocol/) is an application layer protocol that uses HTTPS to exchange payment information. Every payment pointer issued by Rafiki serves as an SPSP endpoint by default (`ENABLE_SPSP_PAYMENT_POINTERS`).
When a `GET` request is made to a payment pointer, the response contains the ILP address of the destination account and a shared secret. These details are needed to set up a STREAM connection between two counterparties to facilitate direct payments over Interledger.
## [STREAM Protocol](#stream-protocol)
[Section titled “STREAM Protocol”](#stream-protocol)
The [STREAM Protocol](https://interledger.org/developers/rfcs/stream-protocol/) is the transport layer protocol in the Interledger Protocol stack. After SPSP communicates the ILP address of the destination account and the shared secret, the STREAM protocol uses these details to set up a STREAM connection between the counterparties.
Rafiki’s `backend` service includes an Interledger connector for sending and receiving STREAM packets. STREAM packets are encoded, encrypted, and sent as the `data` field in ILP packets. The protocol uses the shared secret to authenticate and encrypt packets, and to generate conditions and fulfillments.
A critical function of STREAM is to determine the path exchange rate and handle any changes in the rate. Rafiki’s exchange rates service allows you to provide exchange rate information.
# Open Payments
Rafiki follows the [Open Payments](https://openpayments.dev) standard to enable third-party clients to securely retrieve account information and initiate payments from your customers’ accounts with their consent. The standard describes a uniform way to create and manage grants and resources for [incoming payments, quotes, and outgoing payments](https://openpayments.dev/concepts/resources/#resource-types).
### [Example use case - retrieve account information](#example-use-case---retrieve-account-information)
[Section titled “Example use case - retrieve account information”](#example-use-case---retrieve-account-information)
Some of your customers use a third-party application that allows them to create budgets and monitor their spending. The application can call the Open Payments APIs, enabling it to communicate with any account servicing entity that implements the Open Payments standard. When your customers give the app permission to retrieve their transaction history, the app communicates with your Rafiki instance via the Open Payments APIs to obtain grants from your authorization server and transaction history from your resource server.
### [Further reading](#further-reading)
[Section titled “Further reading”](#further-reading)
We strongly encourage you to familiarize yourself with the Open Payments standard. Extensive documentation is available on the [Open Payments](https://openpayments.dev) website. We recommend you start by reviewing all the pages within the *Intro to Open Payments* section. Here are a few links to get you started.
* [Getting started with Open Payments](https://openpayments.dev/overview/getting-started/)
* [Client keys](https://openpayments.dev/identity/client-keys/)
* [HTTP message signatures](https://openpayments.dev/identity/http-signatures/)
* [Grant negotiation and authorization](https://openpayments.dev/identity/grants/)
## [Rafiki’s backend service](#rafikis-backend-service)
[Section titled “Rafiki’s backend service”](#rafikis-backend-service)
Rafiki’s [`backend`](/v1-beta/integration/deployment/services/backend-service) service is the main service for handling business logic and external communication. The service is responsible for, among other things, exposing the endpoints of the Open Payments APIs for clients to perform account management tasks. Every request and response is validated against the [Open Payments specification](https://github.com/interledger/open-payments/tree/main/openapi).
## [Rafiki’s auth service](#rafikis-auth-service)
[Section titled “Rafiki’s auth service”](#rafikis-auth-service)
Rafiki’s [`auth`](/v1-beta/integration/deployment/services/auth-service) service is a reference implementation of an opinionated Open Payments authorization server. The authorization server is responsible for delegating authorization (via grants) to clients to use the Open Payments APIs, resolving [clients’ public keys](https://openpayments.dev/identity/client-keys/) to authenticate and authorize incoming requests, and creating payments and quotes on the backend. Open Payments leverages the Grant Negotiation and Authorization Protocol (GNAP) for delegating authorization. You can learn more about the protocol by reviewing its [specification](https://datatracker.ietf.org/doc/html/draft-ietf-gnap-core-protocol).
# Payment pointers and wallet addresses
## [Payment pointers](#payment-pointers)
[Section titled “Payment pointers”](#payment-pointers)
A payment pointer is a standardized identifier for a payment account that supports Interledger payments. Each payment pointer must resolve to an HTTPS URL that serves as an [SPSP](/v1-beta/overview/concepts/interledger#simple-payment-setup-protocol-spsp) endpoint to facilitate sending and receiving ILP packets.
You can determine whether a URL is a payment pointer by sending a `GET` request to the URL with an `accept: application/spsp4+json` header.
Example request
```http
curl --request GET \
--url https://wallet.example.com/alice/ \
--header 'accept: application/spsp4+json'
```
A response from an SPSP server means the URL is a payment pointer.
Example response
```http
{
"destination_account":"example.0.cloudnine.ind.alice.cdfa5e16-e759",
"shared_secret":"7h0s7EpQDqcgzqmX-mwrNHFHinPvJq8Jw",
}
```
Payment pointers are often written out using the `$` shorthand. For example, `$wallet.example.com/alice`, which resolves to `https://wallet.example.com/alice/`.
Rafiki assigns each of your customers’ accounts with a payment pointer. This payment pointer is also a wallet address because Rafiki supports both Interledger and Open Payments.
## [Wallet addresses](#wallet-addresses)
[Section titled “Wallet addresses”](#wallet-addresses)
A wallet address is a secure, unique URL for a payment account that supports Open Payments. It acts as an entry point into the Open Payments APIs, facilitating interactions like sending and receiving payments.
You can determine whether a URL is a wallet address by sending a `GET` request to the URL with an `accept: application/json` header.
Example request
```http
curl --request GET \
--url https://wallet.example.com/alice \
--header 'accept: application/json'
```
A valid response means the URL is a wallet address.
Example response
```http
{
"id": "https://wallet.example.com/alice",
"publicName": "Alice",
"assetCode": "USD",
"assetScale": 2,
"authServer": "https://auth.wallet.example.com",
"resourceServer": "https://wallet.example.com",
}
```
Rafiki assigns each of your customers’ accounts with a wallet address. This wallet address is also a payment pointer because Rafiki supports Open Payments and Interledger. See the integration requirements for [wallet addresses](/v1-beta/integration/requirements/wallet-addresses) for more information.
# Telemetry
The objective of the telemetry feature is to gather metrics and establish an infrastructure for visualizing valuable network insights. Some of the metrics that we at the Interledger Foundation collect include:
* The total amount of money transferred via packet data within a specified time frame (daily, weekly, monthly)
* The number of transactions that have been at least partially successful
* The number of ILP packets flowing through the network
* The average amount of money held within the network per transaction
* The average time it takes for an outgoing payment to complete
Our goals are to:
* Track the growth of the network in terms of transaction sizes and the number of transactions processed
* Use the data for our own insights
* Enable you to gain your own insights
### [Privacy and optionality](#privacy-and-optionality)
[Section titled “Privacy and optionality”](#privacy-and-optionality)
Privacy is a paramount concern for the Interledger Foundation. Rafiki’s telemetry feature is designed to provide valuable network insights without violating privacy or aiding malicious ASEs. Review the [Privacy](#privacy) section below for more information.
The telemetry feature is currently enabled by default on test environments (environments not dealing with real money). When active, the feature transmits metrics to the testnet collector. You can opt in to sharing your metrics with a livenet collector when operating in a production livenet environment (with real money). Regardless of environment, you can also opt-out of telemetry completely. Review the [telemetry environment variables](#telemetry-environment-variables) for more information.
### [Architecture](#architecture)
[Section titled “Architecture”](#architecture)

### [OpenTelemetry (OTEL)](#opentelemetry-otel)
[Section titled “OpenTelemetry (OTEL)”](#opentelemetry-otel)
The Interledger Foundation has adopted [OpenTelemetry (OTEL)](https://opentelemetry.io/) to ensure compliance with a standardized framework that is compatible with a variety of tool suites. OTEL allows you to use your preferred tools for data analysis, while Rafiki is instrumented and observable through a standardized metrics format.
### [Telemetry Elastic Container Service (ECS) cluster](#telemetry-elastic-container-service-ecs-cluster)
[Section titled “Telemetry Elastic Container Service (ECS) cluster”](#telemetry-elastic-container-service-ecs-cluster)
The Telemetry Replica service is hosted on AWS ECS Fargate and is configured for availability and load balancing of custom ADOT (AWS Distro for OpenTelemetry) Collector ECS tasks.
When you opt for telemetry, metrics are sent to our Telemetry service. To enable you to build your own telemetry solutions, instrumented Rafiki can send data to multiple endpoints. This allows for the integration of a local [OTEL Collector](https://opentelemetry.io/docs/collector/) container that can support custom requirements. Metrics communication is facilitated through [gRPC](https://grpc.io/).
### [OTEL SDK - Rafiki instrumentation](#otel-sdk---rafiki-instrumentation)
[Section titled “OTEL SDK - Rafiki instrumentation”](#otel-sdk---rafiki-instrumentation)
The OTEL SDK is integrated into Rafiki to create, collect, and export metrics. The SDK integrates seamlessly with the OTEL Collector.
### [Prometheus - AMP](#prometheus---amp)
[Section titled “Prometheus - AMP”](#prometheus---amp)
The Interledger Foundation uses Amazon Managed Service for Prometheus (AMP) to collect data from the telemetry cluster.
Note
AMP offers limited configuration options and cannot crawl data outside of AWS. This limitation led us to adopt a push model, using `prometheusRemoteWrite`, instead of a pull model. For future development, we may consider hosting our own Prometheus.
### [Grafana - Grafana Cloud](#grafana---grafana-cloud)
[Section titled “Grafana - Grafana Cloud”](#grafana---grafana-cloud)
Grafana Cloud is used for data visualization dashboards and offers multiple tools that extend Prometheus Promql.
Note
The Interledger Foundation initially used Amazon-hosted Grafana which did not meet our needs for embedding dashboards. Grafana Cloud offers a feature called *public dashboards* which allows us to share dashboards. However, embedding may still pose a challenge.
### [Exchange rates](#exchange-rates)
[Section titled “Exchange rates”](#exchange-rates)
For telemetry purposes, all amounts collected by instrumented Rafiki should be converted to a base currency.
Privacy reasoning
If only two ASEs are peered over a non-USD currency and we collect data in that currency, it would be easy to determine the volumes moved between those two ASEs. To maintain privacy, we convert all amounts to a base currency.
If an ASE does not provide the necessary exchange rate for a transaction, the telemetry solution still converts the amount to the base currency using external exchange rates. A Lambda function on AWS retrieves and stores the external exchange rates. The function is triggered by a daily `CloudWatch` event and stores the rates in a public S3 bucket. The S3 bucket does not have versioning, and the data is overwritten daily to further ensure privacy.
### [Instrumentation](#instrumentation)
[Section titled “Instrumentation”](#instrumentation)
Rafiki has the following metrics. All data points (counter increases) are exported to collection endpoints at a configurable interval. The default interval is 15 seconds.
| Metric | Type | Description | Behavior |
| ------------------------- | --------- | ------------------------------------------ | ------------------------------------------------------------------------------ |
| `transactions_total` | Counter | Count of funded outgoing transactions | Increases by 1 for each successfully funded outgoing payment resource |
| `packet_count_prepare` | Counter | Count of ILP Prepare packets that are sent | Increases by 1 for each Prepare packet that’s sent |
| `packet_count_fulfill` | Counter | Count of ILP Fulfill packets | Increases by 1 for each Fulfill packet that’s received |
| `packet_count_reject` | Counter | Count of ILP Reject packets | Increases by 1 for each Reject packet that’s received |
| `packet_amount_fulfill` | Counter | Amount sent through the network | Increases by the amount sent in each ILP packet |
| `transaction_fee_amounts` | Counter | Fee amount sent through network | Increases by the amount sent minus the amount received for an outgoing payment |
| `ilp_pay_time_ms` | Histogram | Time to complete an ILP payment | Records the time taken to make an ILP payment |
The current implementation only collects metrics on the SENDING side of a transaction. Metrics for external Open Payments transactions RECEIVED by a Rafiki instance in the network are not collected.
## [Privacy](#privacy)
[Section titled “Privacy”](#privacy)
Rafiki telemetry is designed with a strong emphasis on privacy. The system anonymizes user data and refrains from collecting identifiable information. Since transactions can originate from any user to a Rafiki instance, the privacy measures are implemented directly at the source (each Rafiki instance). This means that at the individual level, the data is already anonymous as single Rafiki instances service transactions for multiple users.
### [Differential privacy and local differential privacy (LDP)](#differential-privacy-and-local-differential-privacy-ldp)
[Section titled “Differential privacy and local differential privacy (LDP)”](#differential-privacy-and-local-differential-privacy-ldp)
Differential privacy is a system for publicly sharing information about a dataset by describing the patterns of groups within the dataset while withholding information about individuals in the dataset. Local differential privacy (LDP) is a variant of differential privacy where noise is added to each individual’s data point before the data point is sent to the server. This ensures that the server never sees the actual data, providing a strong privacy guarantee.
### [Rounding technique and bucketing](#rounding-technique-and-bucketing)
[Section titled “Rounding technique and bucketing”](#rounding-technique-and-bucketing)
Rafiki’s telemetry implementation uses a rounding technique that essentially aggregates multiple transactions into the same value, making them indistinguishable. This is achieved by dividing the transaction values into buckets and rounding the values to the nearest bucket.
The bucket size is calculated based on the raw transaction value. For lower value transactions, which are expected to occur more frequently, the bucket sizes are determined linearly for higher granularity. However, after a certain threshold, the bucket size calculation switches to a logarithmic function to ensure privacy for higher value transactions (which are less frequent but pose greater privacy concerns).
To handle outliers, a clipping technique is implemented, capping the buckets. Any value that exceeds a given threshold is placed in a single bucket. Conversely, any value that falls below a certain minimum is also placed in a single bucket. This ensures that both high and low outliers do not disproportionately affect the overall data, providing further privacy guarantees for these transactions.
### [Laplacian distribution](#laplacian-distribution)
[Section titled “Laplacian distribution”](#laplacian-distribution)
The Laplacian distribution is often used in differential privacy due to its double exponential decay property. This property ensures that a small change in the data does not significantly affect the probability distribution of the output, providing a strong privacy guarantee.
To achieve local differential privacy (LDP), noise is selected from the Laplacian distribution and added to the rounded values. The noise is generated based on a privacy parameter, which is calculated using the sensitivity of the function.
The sensitivity of a function in differential privacy is the maximum amount that any single observation can change the output of the function. In this case, the sensitivity is considered to be the maximum of the rounded value and the bucket size.
The privacy parameter is computed as one-tenth of the sensitivity. This parameter controls the trade-off between privacy and utility: a smaller privacy parameter means more privacy but less utility, and a larger privacy parameter means less privacy but more utility.
The noise, selected from the Laplacian distribution, is then generated using this privacy parameter and added to the rounded value. If the resulting value is zero, the value is set to half the bucket size to ensure that the noise does not completely obscure the transaction value.
### [Currency conversion](#currency-conversion)
[Section titled “Currency conversion”](#currency-conversion)
Another factor that obscures sensitive data is currency conversion. In cross-currency transactions, exchange rates are provided by you, as the ASE, internally. As such, the exchange rates cannot be correlated to an individual transaction. If you don’t or can’t provide the necessary rates, an external API for exchange rates is used. The obtained exchange rates are overwritten frequently in this case, with no versioning or history access. This introduces an additional layer of noise and further protects the privacy of the transactions.
### [Experimental transaction values when using the algorithm](#experimental-transaction-values-when-using-the-algorithm)
[Section titled “Experimental transaction values when using the algorithm”](#experimental-transaction-values-when-using-the-algorithm)
The following table shows the values in the algorithm when running transactions for different amounts. The raw value increases as you move down the rows of the table. All values are in scale 4.
| Raw value | Bucket size | Rounded value | Privacy parameter | Laplace noise | Final value |
| --------- | ----------- | ------------- | ----------------- | ------------- | ----------- |
| 8300 | 10000 | 10000 | 1000 | 2037 | 12037 |
| 13200 | 15000 | 15000 | 1500 | 1397 | 16397 |
| 147700 | 160000 | 160000 | 16000 | -27128 | 132872 |
| 1426100 | 2560000 | 2560000 | 256000 | -381571 | 2178429 |
| 1788200 | 2560000 | 2560000 | 256000 | 463842 | 3023842 |
| 90422400 | 10000000 | 90000000 | 1000000 | 2210649 | 92210649 |
| 112400400 | 10000000 | 100000000 | 1000000 | 407847 | 100407847 |
| 222290500 | 10000000 | 100000000 | 1000000 | -686149 | 99313851 |
### [References](#references)
[Section titled “References”](#references)
Rafiki’s telemetry solution is a combination of techniques described in various white papers on privacy-preserving data collection. More information can be found in the following papers:
* [Local differential privacy for human-centered computing](https://jwcn-eurasipjournals.springeropen.com/articles/10.1186/s13638-020-01675-8)
* [Collecting telemetry data privately](https://www.microsoft.com/en-us/research/blog/collecting-telemetry-data-privately/)
* [RAPPOR: Randomized aggregatable privacy-preserving ordinal response](https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/42852.pdf)
## [Deploy custom telemetry](#deploy-custom-telemetry)
[Section titled “Deploy custom telemetry”](#deploy-custom-telemetry)
Rafiki allows you to build your own telemetry solution based on the [OpenTelemetry (OTEL)](https://opentelemetry.io/) standardized metrics format that Rafiki exposes.
You must deploy your own OTEL Collector that acts as a sidecar container to Rafiki, then provide the OTEL Collector’s ingest endpoint so that Rafiki can begin sending metrics to the collector.
### [Telemetry environment variables](#telemetry-environment-variables)
[Section titled “Telemetry environment variables”](#telemetry-environment-variables)
#### [Required](#required)
[Section titled “Required”](#required)
When the `ENABLE_TELEMETRY` variable is `true`, the following are required.
| Variable name | Type | Description |
| --------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `INSTANCE_NAME` | String | Your Rafiki instance’s name used to communicate for telemetry and auto-peering. For telemetry, it’s used to distinguish between the different instances pushing data to the telemetry collector. |
#### [Optional](#optional)
[Section titled “Optional”](#optional)
| Variable name | Type | Description |
| -------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ENABLE_TELEMETRY` | Boolean | Enables the telemetry service on Rafiki. Defaults to `true`. |
| `LIVENET` | Boolean | Determines where to send metrics. Defaults to `false`, resulting in metrics being sent to the testnet OTEL Collector.Set to `true` on production environments dealing with real money. |
| `OPEN_TELEMETRY_COLLECTOR_URLS` | String | A CSV of URLs for OTEL Collectors (e.g., `http://otel-collector-NLB-e3172ff9d2f4bc8a.elb.eu-west-2.amazonaws.com:4317,http://happy-life-otel-collector:4317`). |
| `OPEN_TELEMETRY_EXPORT_INTERVAL` | Number | Indicates, in milliseconds, how often the instrumented Rafiki instance should send metrics. Defaults to`15000`. |
| `TELEMETRY_EXCHANGE_RATES_URL` | String | Defines the endpoint Rafiki queries for exchange rates. Used as a fallback if/when [exchange rates](/v1-beta/integration/requirements/exchange-rates) aren’t provided.When set, the response format of the external exchange rates API should be of type `rates`, as is expected by the rate service.Defaults to `https://telemetry-exchange-rates.s3.amazonaws.com/exchange-rates-usd.json`, which points to a public S3 that has the previously mentioned required format, updated daily. |
### [Example Docker OTEL Collector image and configuration](#example-docker-otel-collector-image-and-configuration)
[Section titled “Example Docker OTEL Collector image and configuration”](#example-docker-otel-collector-image-and-configuration)
Below is an example of a Docker OTEL Collector image and configuration that integrates with Rafiki and sends data to a Prometheus remote write endpoint.
You can test the configuration in our [Local Playground](/v1-beta/integration/playground/overview) by providing the environment variables in the preceding table to `happy-life-backend` in the [`docker-compose.yml`](https://github.com/interledger/rafiki/blob/main/localenv/happy-life-bank/docker-compose.yml) file.
#### [Docker Compose config](#docker-compose-config)
[Section titled “Docker Compose config”](#docker-compose-config)
```yaml
# Serves as example for optional local collector configuration
happy-life-otel-collector:
image: otel/opentelemetry-collector-contrib:latest
command: ['--config=/etc/otel-collector-config.yaml', '']
environment:
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID-''}
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY-''}
volumes:
- ../collector/otel-collector-config.yaml:/etc/otel-collector-config.yaml
networks:
- rafiki
expose:
- 4317
ports:
- '13132:13133' # health_check extension
```
#### [OTEL Collector config](#otel-collector-config)
[Section titled “OTEL Collector config”](#otel-collector-config)
Supplemental documentation is available in [OTEL’s Collector Configuration](https://opentelemetry.io/docs/collector/configuration/) documentation.
```yaml
# Serves as example for the configuration of a local OpenTelemetry Collector that sends metrics to an AWS Managed Prometheus Workspace
# Sigv4auth required for AWS Prometheus Remote Write access (USER with access keys needed)
extensions:
sigv4auth:
assume_role:
arn: 'arn:aws:iam::YOUR-ROLE:role/PrometheusRemoteWrite'
sts_region: 'YOUR-REGION'
receivers:
otlp:
protocols:
grpc:
http:
cors:
allowed*origins:
- http://*
- https://\_
processors:
batch:
exporters:
logging:
verbosity: 'normal'
prometheusremotewrite:
endpoint: 'https://aps-workspaces.YOUR-REGION.amazonaws.com/workspaces/ws-YOUR-WORKSPACE-IDENTIFIER/api/v1/remote_write'
auth:
authenticator: sigv4auth
service:
telemetry:
logs:
level: 'debug'
metrics:
level: 'detailed'
address: 0.0.0.0:8888
extensions: [sigv4auth]
pipelines:
metrics:
receivers: [otlp]
processors: [batch]
exporters: [logging, prometheusremotewrite]
```
# Overview
Implementing and maintaining the [Interledger Protocol (ILP)](#interledger) stack on your own can be difficult and time-consuming. Rafiki makes it easy to integrate with the Interledger network without needing to develop and maintain your own implementations.
Rafiki is open-source software maintained by a dedicated team and freely available to any licensed [account servicing entity](/v1-beta/overview/concepts/account-servicing-entity)?A regulated entity that provides and maintains payment accounts for its customers (ASE) wanting to implement Interledger and [Open Payments](#open-payments) on users’ accounts.
Try it out
The [Local Playground](/v1-beta/integration/playground/overview) allows you to test Rafiki by running two mock account servicing entities that automatically peer with one another.
## [Use cases](#use-cases)
[Section titled “Use cases”](#use-cases)
### [Peer-to-peer payments between ASEs](#peer-to-peer-payments-between-ases)
[Section titled “Peer-to-peer payments between ASEs”](#peer-to-peer-payments-between-ases)
In the context of Rafiki, a peer is another ASE with whom you transact. Forming a peering relationship requires you to both agree on the currency in which you will transact, on a settlement mechanism and cadence, and other details. Interledger creates interoperability between different payment systems and currencies, making it easier for peers to directly transact with one another.
### [eCommerce payments](#ecommerce-payments)
[Section titled “eCommerce payments”](#ecommerce-payments)
If a merchant accepts Interledger or Open Payments as a payment method, then a customer can pay using their wallet address instead of entering, for example, a credit card number and other personal details on the merchant’s site. Rafiki’s implementation of Interledger and Open Payments means your account holders can use their wallet addresses for both one-time purchases and recurring purchases, such as subscriptions, anywhere Interledger or Open Payments is an accepted payment method.
### [Web Monetization](#web-monetization)
[Section titled “Web Monetization”](#web-monetization)
With [Web Monetization](https://webmonetization.org), site visitors can pay an amount of their choosing to a participating site with little to no interaction. Both the site and the site visitor must have an Open Payments-enabled wallet address to receive and send payments. Rafiki’s implementation of Interledger’s Simple Payment Setup Protocol (SPSP) and the Open Payments standard means you can assign one or more wallet addresses to your account holders’ accounts, making these accounts support incoming and outgoing Web Monetization payments right out of the box.
## [Interledger](#interledger)
[Section titled “Interledger”](#interledger)
The Interledger network is a network of nodes that have implemented the Interledger Protocol (ILP) stack. Rafiki is a reference implementation if the ILP stack, enabling you to more easily become a node on the network and start sending and receiving payments.
Interledger is designed to be a network on top of existing payment networks that serves as the interoperability layer between them all, forwarding payment messages (packets) while also taking care of currency conversion. Interledger ensures that packets take the fastest and cheapest route from one Interledger node to another.
[Learn more about Interledger](/v1-beta/overview/concepts/interledger)
## [Open Payments](#open-payments)
[Section titled “Open Payments”](#open-payments)
Open Payments is an API standard and set of open RESTful APIs that facilitate interoperability in the setup and completion of payments. The standard provides a uniform way to create and manage grants and resources for incoming payments, quotes, and outgoing payments.
By following the Open Payments standard, Rafiki allows your customers’ accounts to become Open Payments-enabled. Clients, such as mobile apps, can then call the Open Payments APIs to securely retrieve transaction data and initiate payments from your customers’ accounts with your customers’ prior consent.
[Learn more about Open Payments](/v1-beta/overview/concepts/open-payments)
# Architecture
Rafiki is a collection of three services that run together. Each one can scale horizontally.
* [Backend](/v1-beta/integration/deployment/services/backend-service) - The main service, responsible for handling business logic and external communication
* [Auth](/v1-beta/integration/deployment/services/auth-service) - A reference implementation of an Open Payments authorization server, used for grant authorization and authentication
* [Frontend](/v1-beta/integration/deployment/services/frontend-service) - An optional internal user interface, called the [Rafiki Admin](/v1-beta/admin/admin-user-guide), for you to manage your Rafiki instance
These services rely on a number of databases.
* A Postgres database used by the `auth` service for storing auth-related resources (grants, access tokens, and interactions)
* A Redis database used by the `auth` service to store session data
* A Postgres database used by the `backend` service for Open Payments resources and application data
* [TigerBeetle](https://github.com/tigerbeetle/tigerbeetle)
, used by the `backend` service for accounting balances
* A Redis database used by the `backend` service as a cache to share STREAM connection details across processes
An additional package for [token introspection](/v1-beta/integration/deployment/services/auth-service#token-introspection) is also included with Rafiki. This is an internal package that requires no action on your part if you’re using Rafiki’s `auth` service.

# Environment variables
Environment variables are key value pairs used to configure how your Rafiki instance will run within your infrastructure and integrate with your systems.
Each environment variable name is uppercase, followed by an equal sign and the value of the variable.
Environment variable example
```bash
WEBHOOKS_URL=http://my-business/webhooks
```
The environment variable in the preceding example specifies the HTTP endpoint at which you want your Rafiki instance to send you notifications of webhook events.
To run Rafiki you must set the environment variables for the `backend`, `auth` and `frontend` services where listed as required below.
Caution
If you are running Rafiki via Docker, you must update the variables in the `docker-compose.yml` files of the respective services.
If you are running Rafiki via Helm and Kubernetes, you must update the variables in the [values.yaml](https://github.com/interledger/rafiki/blob/main/infrastructure/helm/rafiki/values.yaml) file of the Helm chart template.
## [Backend](#backend)
[Section titled “Backend”](#backend)
### [Required](#required)
[Section titled “Required”](#required)
| Variable | Helm value name | Default | Description |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `AUTH_SERVER_GRANT_URL` | `backend.serviceUrls.AUTH_SERVER_GRANT_URL` | *undefined* | The endpoint on your Open Payments authorization server to grant a request. |
| `AUTH_SERVER_INTROSPECTION_URL` | `backend.serviceUrls.AUTH_SERVER_INTROSPECTION_URL` | *undefined* | The endpoint on your Open Payments authorization server to introspect an access token. |
| `DATABASE_URL` | `backend.postgresql.host`, `backend.postgresql.port`, `backend.postgresql.username`, `backend.postgresql.database`, `backend.postgresql.password` | `postgresql://postgres:password@localhost:5432/development` | The Postgres database URL of the database storing your resource data. For Helm, these components are provided individually. |
| `EXCHANGE_RATES_URL` | `backend.serviceUrls.EXCHANGE_RATES_URL` | *undefined* | The endpoint your Rafiki instance uses to request exchange rates. |
| `ILP_ADDRESS` | `backend.ilp.address` | *undefined* | The ILP address of your Rafiki instance. |
| `ILP_CONNECTOR_URL` | `backend.ilp.connectorUrl` | *undefined* | The ILP connector address where ILP packets are received. |
| `KEY_ID` | `backend.key.id` | *undefined* | Your Rafiki instance’s client key ID. |
| `OPEN_PAYMENTS_URL` | `backend.serviceUrls.OPEN_PAYMENTS_URL` | *undefined* | The public endpoint of your Open Payments resource server. |
| `REDIS_URL` | `backend.redis.host`, `backend.redis.port` | `redis://127.0.0.1:6379` | The Redis URL of the database handling ILP packet data. For Helm, these components are provided individually. |
| `USE_TIGERBEETLE` | `backend.use.tigerbeetle` | `true` | When `true`, a TigerBeetle database is used for accounting. When `false`, a Postgres database is used. |
| `WEBHOOK_URL` | `backend.serviceUrls.WEBHOOK_URL` | *undefined* | Your endpoint that consumes webhook events. |
| `AUTH_SERVICE_API_URL` | `backend.serviceUrls.AUTH_SERVICE_API_URL` | *undefined* | The service-to-service api endpoint on your Open Payments authorization server. |
### [Conditionally required](#conditionally-required)
[Section titled “Conditionally required”](#conditionally-required)
| Variable | Helm value name | Default | Description |
| --------------- | ----------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `INSTANCE_NAME` | `backend.instance.name` | *undefined* | Your Rafiki instance’s name used to communicate for auto-peering and/or [telemetry](/overview/concepts/telemetry). Required when auto-peering and/or telemetry is enabled |
| `TRUST_PROXY` | `backend.trustProxy` | `false` | Must be set to `true` when running Rafiki behind a proxy. When `true`, the `X-Forwarded-Proto` header is used to determine if connections are secure. |
### [Optional](#optional)
[Section titled “Optional”](#optional)
| Variable | Helm value name | Default | Description |
| ----------------------------------------------------- | -------------------------------------------------------- | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ADMIN_PORT` | `backend.port.admin` | `3001` | The port of your Backend Auth API server. |
| `ADMIN_API_SIGNATURE_TTL_SECONDS` | *undefined* | `30` | The TTL, in seconds, for which a request’s signature will be valid. |
| `API_SECRET` | *undefined* | *undefined* | N/A |
| `API_SIGNATURE_VERSION` | *undefined* | `1` | The version of the request signing algorithm used to generate signatures. |
| `AUTO_PEERING_SERVER_PORT` | `backend.autoPeering.serverPort` | `3005` | If auto-peering is enabled, the server will use this port. |
| `CONNECTOR_PORT` | `backend.port.connector` | `3002` | The port of the ILP connector for sending packets via ILP over HTTP. |
| `ENABLE_AUTO_PEERING` | `backend.enable.autoPeering` | `false` | When `true`, auto-peering is enabled. |
| `ENABLE_MANUAL_MIGRATIONS` | `backend.enableManualMigrations` | `false` | When `true`, you must run the database manually with the command `npm run knex – migrate:latest –env production` |
| `ENABLE_SPSP_PAYMENT_POINTERS` | `backend.enable.spspPaymentPointers` | `true` | When `true`, the SPSP route is enabled. |
| `ENABLE_TELEMETRY` | *undefined* | `false` | Enables the telemetry service on Rafiki. |
| `ENABLE_TELEMETRY_TRACES` | *undefined* | `false` | N/A |
| `EXCHANGE_RATES_LIFETIME` | `backend.lifetime.exchangeRate` | `15_000` | The time, in milliseconds, the exchange rates you provide via the `EXCHANGE_RATES_URL` are valid. |
| `GRAPHQL_IDEMPOTENCY_KEY_LOCK_MS` | `backend.idempotency.keyLockMs` | `2000` | The TTL, in milliseconds, for `idempotencyKey` concurrency lock on GraphQL mutations on the Backend Admin API. |
| `GRAPHQL_IDEMPOTENCY_KEY_TTL_MS` | `backend.idempotency.keyTTL` | `86400000` (24 hours) | The TTL, in milliseconds, for `idempotencyKey` on GraphQL mutations on the Backend Admin API. |
| `INCOMING_PAYMENT_CREATED_POLL_FREQUENCY_MS` | *undefined* | `1000` | N/A |
| `INCOMING_PAYMENT_CREATED_POLL_TIMEOUT_MS` | *undefined* | `10000` | N/A |
| `INCOMING_PAYMENT_EXPIRY_MAX_MS` | `backend.incomingPayment.expiryMaxMs` | `2592000000` (30 days) | The maximum into the future, in milliseconds, incoming payments expiry can be set to on creation. |
| `INCOMING_PAYMENT_WORKER_IDLE` | `backend.workerIdle` | `200` | The time, in milliseconds, that `INCOMING_PAYMENT_WORKERS` will wait until checking an empty incoming payment request queue again. |
| `INCOMING_PAYMENT_WORKERS` | `backend.workers.incomingPayment` | `1` | The number of workers processing incoming payment requests. |
| `LOG_LEVEL` | `backend.logLevel` | `info` | [Pino log level](https://getpino.io/#/docs/api?id=levels) |
| `MAX_OUTGOING_PAYMENT_RETRY_ATTEMPTS` | *undefined* | `5` | Specifies how many times an outgoing payment is retried before failing completely |
| `NODE_ENVIRONMENT` | `backend.nodeEnv` | `development` | The type of node environment: `development`, `test`, or `production`. |
| `OPEN_PAYMENTS_PORT` | `backend.port.openPayments` | `3003` | The port of your Open Payments resource server. |
| `OPEN_TELEMETRY_COLLECTOR_URLS` | *undefined* | \*undefined | N/A |
| `OPEN_TELEMETRY_EXPORT_INTERVAL` | *undefined* | `15000` | N/A |
| `OPEN_TELEMETRY_TRACE_COLLECTOR_URLS` | *undefined* | *undefined* | N/A |
| `OUTGOING_PAYMENT_WORKER_IDLE` | `backend.workerIdle` | `200` | The time, in milliseconds, that `OUTGOING_PAYMENT_WORKERS` wait until they check an empty outgoing payment request queue again. |
| `OUTGOING_PAYMENT_WORKERS` | `backend.workers.outgoingPayment` | `4` | The number of workers processing outgoing payment requests. |
| `POLL_INCOMING_PAYMENT_CREATED_WEBHOOK` | *undefined* | `false` | N/A |
| `PRIVATE_KEY_FILE` | `backend.key.file` | *undefined* | The path to your Rafiki instance’s client private key. |
| `QUOTE_LIFESPAN` | `backend.lifetime.quote` | `5 * 60_000` (5 minutes) | The time, in milliseconds, an Open Payments quote is valid for. |
| `REDIS_TLS_CA_FILE_PATH` | `backend.redis.tlsCaFile` | `''` | [Redis TLS config](https://redis.io/docs/latest/operate/oss_and_stack/management/security/encryption/) |
| `REDIS_TLS_CERT_FILE_PATH` | `backend.redis.tlsCertFile` | `''` | [Redis TLS config](https://redis.io/docs/latest/operate/oss_and_stack/management/security/encryption/) |
| `REDIS_TLS_KEY_FILE_PATH` | `backend.redis.tlsKeyFile` | `''` | [Redis TLS config](https://redis.io/docs/latest/operate/oss_and_stack/management/security/encryption/) |
| `SIGNATURE_SECRET` | `backend.quoteSignatureSecret` | *undefined* | The secret to generate request header signatures for webhook event requests. |
| `SIGNATURE_VERSION` | `backend.signatureVersion` | `1` | The version number to generate request header signatures for webhook events. |
| `SLIPPAGE` | `backend.ilp.slippage` | `0.01` (1%) | The accepted ILP rate fluctuation. |
| `STREAM_SECRET` | `backend.ilp.streamSecret` | *undefined* | The seed secret to generate shared STREAM secrets. |
| `TELEMETRY_EXCHANGE_RATES_LIFETIME` | *undefined* | `86_400_000` | N/A |
| `TELEMETRY_EXCHANGE_RATES_URL` | *undefined* | `https://telemetry-exchange-rates.s3.amazonaws.com/exchange-rates-usd.json` | The endpoint Rafiki will query for exchange rates. Used as a fallback if/when [exchange rates](/integration/requirements/exchange-rates) aren’t provided. |
| `TIGERBEETLE_CLUSTER_ID` | *undefined* | `0` | The TigerBeetle cluster ID picked by the system that starts the TigerBeetle cluster to create a [TigerBeetle client](https://docs.tigerbeetle.com/clients/node/#creating-a-client). |
| `TIGERBEETLE_REPLICA_ADDRESSES` | *undefined* | `3004` | TigerBeetle replica addresses for all replicas in the cluster. The addresses are comma-separated IP addresses/ports, to create a [TigerBeetle client](https://docs.tigerbeetle.com/clients/node/#creating-a-client). |
| `TIGERBEETLE_REPLICA_ADDRESSES.SPLIT` | *undefined* | `3004` | N/A |
| `TIGERBEETLE_TWO_PHASE_TIMEOUT_SECONDS` | *undefined* | `5` | N/A |
| `WALLET_ADDRESS_DEACTIVATION_PAYMENT_GRACE_PERIOD_MS` | `backend.walletAddress.deactivationPaymentGratePeriodMs` | `86400000` (24 hours) | The time into the future, in milliseconds, to set expiration of Open Payments incoming payments when deactivating a wallet address. |
| `WALLET_ADDRESS_LOOKUP_TIMEOUT_MS` | `backend.walletAddress.lookupTimeoutMs` | `1500` | The time, in milliseconds, you have to create a missing wallet address before timeout. |
| `WALLET_ADDRESS_POLLING_FREQUENCY_MS` | `backend.walletAddress.pollingFrequencyMs` | `100` | The frequency of polling while waiting for you to create a missing wallet address. |
| `WALLET_ADDRESS_URL` | `backend.serviceUrls.WALLET_ADDRESS_URL` | `http://127.0.0.1:3001/.well-known/pay` | Your Rafiki instance’s internal wallet address. |
| `WALLET_ADDRESS_WORKER_IDLE` | `backend.workerIdle` | `200` | The time, in milliseconds, that `WALLET_ADDRESS_WORKERS` wait until checking the empty wallet address request queue again. |
| `WALLET_ADDRESS_WORKERS` | \`backend.workers.walletAddress | `1` | The number of workers processing wallet address requests. |
| `WEBHOOK_MAX_RETRY` | `backend.webhookMaxRetry` | `10` | The maximum number of times your Rafiki instance’s backend retries sending a certain webhook event to your configured `WEBHOOK_URL`. |
| `WEBHOOK_TIMEOUT` | `backend.lifetime.webhook` | `2000` (2 seconds) | The time, in milliseconds, that your Rafiki instance will wait for a `200` response from your webhook endpoint. If a `200` response is not received, Rafiki will time out and try to send the webhook event again. |
| `WEBHOOK_WORKER_IDLE` | `backend.workerIdle` | `200` | The time, in milliseconds, that `WEBHOOK_WORKERS` will wait until they check the empty webhook event queue again. |
| `WEBHOOK_WORKERS` | `backend.workers.webhook` | `1` | The number of workers processing webhook events. |
| `WITHDRAWAL_THROTTLE_DELAY` | `backend.withdrawalThrottleDelay` | *undefined* | The delay in liquidity withdrawal processing. |
## [Auth](#auth)
[Section titled “Auth”](#auth)
### [Required](#required-1)
[Section titled “Required”](#required-1)
| Variable | Helm value name | Default | Description |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AUTH_DATABASE_URL` | `auth.postgresql.host`, `auth.postgresql.port`, `auth.postgresql.username`, `auth.postgresql.database`, `auth.postgresql.password` | `postgresql://postgres:password@localhost:5432/auth_development` | The URL of the Postgres database storing your Open Payments grant data. For Helm, these components are provided individually. |
| `AUTH_SERVER_URL` | `auth.server.domain` | *undefined* | The public endpoint for your Rafiki instance’s public Open Payments routes. |
| `COOKIE_KEY` | `auth.cookieKey` | *undefined* | The [koa KeyGrip key](https://koajs.com/#app-keys-) that is used to sign cookies for an interaction session. |
| `IDENTITY_SERVER_URL` | `auth.identityServer.domain` | *undefined* | The URL of your IdP’s server, used by the authorization server to inform an Open Payments client of where to redirect the end-user to start interactions. |
| `IDENTITY_SERVER_SECRET` | `auth.identityServer.secret` | *undefined* | A shared secret between the authorization server and the IdP server; the authorization server will use the secret to secure its IdP-related endpoints. When the IdP server sends requests to the authorization server, the IdP server must provide the secret via an [`x-idp-secret`](/integration/requirements/open-payments/idp#x-idp-secret-header) header. |
| `REDIS_URL` | `auth.redis.host`, `auth.redis.port` | `redis://127.0.0.1:6379` | The connection URL for Redis. For Helm, these components are provided individually. |
### [Conditionally required](#conditionally-required-1)
[Section titled “Conditionally required”](#conditionally-required-1)
| Variable | Helm value name | Default | Description |
| ------------- | ----------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TRUST_PROXY` | `auth.trustProxy` | `false` | Must be set to `true` when running Rafiki behind a proxy. When `true`, the `X-Forwarded-Proto` header is used to determine if connections are secure. |
### [Optional](#optional-1)
[Section titled “Optional”](#optional-1)
| Variable | Helm value name | Default | Description |
| --------------------------------- | ----------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ACCESS_TOKEN_DELETION_DAYS` | `auth.accessToken.deletionDays` | `30` | The days until expired and/or revoked access tokens are deleted. |
| `ACCESS_TOKEN_EXPIRY_SECONDS` | `auth.accessToken.expirySeconds` | `600` (10 minutes) | The expiry time, in seconds, for access tokens. |
| `ADMIN_API_SIGNATURE_VERSION` | `auth.adminApi.signatureVersion` | `1` | The version of the request signing algorithm used to generate signatures. |
| `ADMIN_API_SIGNATURE_TTL_SECONDS` | `auth.adminAPI.signatureTtlSeconds` | `30` | The TTL, in seconds, for which a request’s signature will be valid. |
| `ADMIN_PORT` | `auth.port.admin` | `3003` | The port of your Rafiki Auth Admin API server. |
| `AUTH_PORT` | `auth.port.auth` | `3006` | The port of your Open Payments authorization server. |
| `DATABASE_CLEANUP_WORKERS` | `auth.workers.cleanup` | `1` | The number of workers processing expired or revoked access tokens. |
| `ENABLE_MANUAL_MIGRATIONS` | `auth.enableManualMigrations` | `false` | When `true`, you must run the auth Postgres database manually with the command `npm run knex – migrate:latest –envproduction` |
| `INCOMING_PAYMENT_INTERACTION` | `auth.interaction.incomingPayment` | `false` | When `true`, incoming Open Payments grant requests are interactive |
| `INTERACTION_EXPIRY_SECONDS` | `auth.interactionExpirySeconds` | `600` (10 minutes) | The time, in seconds, for which a user can interact with a grant request before the request expires. |
| `INTERACTION_PORT` | `auth.port.interaction` | `3009` | The port number of your Open Payments interaction-related APIs. |
| `INTROSPECTION_PORT` | `auth.port.introspection` | `3007` | The port of your Open Payments access token introspection server. |
| `SERVICE_API_PORT` | `auth.port.serviceAPIPort` | `3011` | The port to expose the internal service api. |
| `LIST_ALL_ACCESS_INTERACTION` | `auth.interaction.listAll` | `true` | When `true`, grant requests that include a `list-all` action will require interaction. In these requests, the client asks to list resources that it did not create. |
| `LOG_LEVEL` | `auth.logLevel` | `info` | [Pino log level](https://getpino.io/#/docs/api?id=levels) |
| `NODE_ENV` | `auth.nodeEnv` | `development` | The type of node environment: `development`, `test`, or `production`. |
| `QUOTE_INTERACTION` | `auth.interaction.quote` | `false` | When `true`, quote grants are interactive. |
| `REDIS_TLS_CA_FILE_PATH` | `auth.redis.tlsCaFile` | `''` | [Redis TLS config](https://redis.io/docs/latest/operate/oss_and_stack/management/security/encryption/) |
| `REDIS_TLS_CERT_FILE_PATH` | `auth.redis.tlsCertFile` | `''` | [Redis TLS config](https://redis.io/docs/latest/operate/oss_and_stack/management/security/encryption/) |
| `REDIS_TLS_KEY_FILE_PATH` | `auth.redis.tlsKeyFile` | `''` | [Redis TLS config](https://redis.io/docs/latest/operate/oss_and_stack/management/security/encryption/) |
| `WAIT_SECONDS` | `auth.grant.waitSeconds` | `5` | The wait time, in seconds, included in a grant request response (`grant.continue`). |
## [Frontend](#frontend)
[Section titled “Frontend”](#frontend)
### [Required](#required-2)
[Section titled “Required”](#required-2)
| Variable | Helm value name | Default | Description |
| ------------------- | ---------------------------------------- | ----------- | -------------------------------------------- |
| `GRAPHQL_URL` | `frontend.serviceUrls.GRAPHQL_URL` | *undefined* | URL for Rafiki’s GraphQL Auth Admin API |
| `OPEN_PAYMENTS_URL` | `frontend.serviceUrls.OPEN_PAYMENTS_URL` | *undefined* | Your Open Payments API endpoint |
| `PORT` | `frontend.port` | *undefined* | Port from which to host the Rafiki Remix app |
### [Conditionally required](#conditionally-required-2)
[Section titled “Conditionally required”](#conditionally-required-2)
The following variables are required only when `AUTH_ENABLED` is set to `true`.
| Variable | Helm value name | Default | Description |
| ----------------------------- | ------------------------------------ | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `KRATOS_ADMIN_URL` | `frontend.kratos.adminUrl` | *undefined* | The admin endpoint/container address for Kratos |
| `KRATOS_CONTAINER_PUBLIC_URL` | `frontend.kratos.containerPublicUrl` | *undefined* | The URL for you to access the Kratos Docker container from within the Docker network. This is used for backend calls to Kratos. |
| `KRATOS_BROWSER_PUBLIC_URL` | `frontend.kratos.browserPublicUrl` | *undefined* | The URL for you to access the Kratos Docker container from a browser outside of the Docker network. This is used for calls from a browser (what you see in the Rafiki Admin UI) to the Kratos server on the backend. |
### [Optional](#optional-2)
[Section titled “Optional”](#optional-2)
| Variable | Helm value name | Default | Description |
| -------------------------------- | -------------------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AUTH_ENABLED` | `frontend.authEnabled` | `true` | When `true`, only authenticated users can be granted access to Rafiki Admin by an administrator |
| `SIGNATURE_SECRET` | `frontend.quoteSignatureSecret` | *undefined* | The signature secret used to authenticate requests to the Backend Admin API. |
| `SIGNATURE_VERSION` | `frontend.signatureVersion` | `1` | The signature version number used to authenticate requests to the Backend Admin API. |
| `ENABLE_INSECURE_MESSAGE_COOKIE` | `frontend.enableInsecureMessageCookie` | `true` | When set to `true`, `t`, or `1`, cookie will be transmitted over insecure HTTP connection. Insecure message cookies are required for flash messages to work over HTTP. |
| `NODE_ENV` | `frontend.nodeEnv` | `production` | The type of node environment: `development`, `test`, or `production`. |
| `LOG_LEVEL` | `frontend.logLevel` | `info` | [Pino log level](https://getpino.io/#/docs/api?id=levels) |
# Get involved
Welcome to the Rafiki community! Whether you’re a seasoned developer or just getting started with open source, there are many ways to contribute to the Rafiki project. This guide will help you find your place in our vibrant ecosystem.
## [🚀 Get started](#-get-started)
[Section titled “🚀 Get started”](#-get-started)
### [New to Rafiki?](#new-to-rafiki)
[Section titled “New to Rafiki?”](#new-to-rafiki)
* **Explore the project**: Browse the [Rafiki repository](https://github.com/interledger/rafiki) to understand the codebase structure and recent activity.
* **Join our community**:
* Connect with us on [`interledger.slack.com`](https://communityinviter.com/apps/interledger/interledger-working-groups-slack) in the `#rafiki` channel for real-time discussions
* Start or join [GitHub Discussions](https://github.com/interledger/rafiki/discussions) for deeper technical conversations
* **Get hands-on**: Try [running the system locally](/v1-beta/integration/playground/overview) to familiarize yourself with Rafiki’s functionality. Don’t hesitate to ask questions in the `#rafiki` Slack channel!
### [Make your first contribution](#make-your-first-contribution)
[Section titled “Make your first contribution”](#make-your-first-contribution)
Ready to dive in? Here’s how to review good first issues on GitHub if you are new to open source development.
#### [1: Navigate to the issues tab](#1-navigate-to-the-issues-tab)
[Section titled “1: Navigate to the issues tab”](#1-navigate-to-the-issues-tab)
Go to [good first issues](https://github.com/interledger/rafiki/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) in the Rafiki repo.
#### [2: Check prerequisites](#2-check-prerequisites)
[Section titled “2: Check prerequisites”](#2-check-prerequisites)
Make sure to review our comprehensive [contribution guide](https://github.com/interledger/rafiki/blob/main/.github/contributing.md)
Before claiming an issue, ensure you have:
* Read the issue description and comments
* Reviewed any linked documentation or related issues
* Understood the expected deliverables
* Confirmed the issue hasn’t been resolved in a recent pull request
## [👨💻 Coding](#-coding)
[Section titled “👨💻 Coding”](#-coding)
* [Contribute](https://github.com/interledger/rafiki) to Rafiki coding or testing.
## [📚 Documentation & learning](#-documentation--learning)
[Section titled “📚 Documentation & learning”](#-documentation--learning)
### [Contribute to our documentation](#contribute-to-our-documentation)
[Section titled “Contribute to our documentation”](#contribute-to-our-documentation)
* Improve existing documentation clarity and accuracy
* Create step-by-step tutorials for common use cases
* Develop troubleshooting guides and FAQ sections
* Translate documentation to the following languages:
* Arabic
* Chinese
* French
* German
* Japanese
* Portuguese
* Spanish
* Review translated and localized content
### [Content creation](#content-creation)
[Section titled “Content creation”](#content-creation)
* Write blog posts about your Rafiki integration experiences
* Create video tutorials or demos
* Develop sample applications and code examples
* Share best practices and patterns you’ve discovered
## [🌟 Community building](#-community-building)
[Section titled “🌟 Community building”](#-community-building)
### [Events and engagement](#events-and-engagement)
[Section titled “Events and engagement”](#events-and-engagement)
* Attend the [Interledger Summit](https://interledger.org/summit) and participate in our annual hackathon
* Organize local meetups, coding sprints, and workshops
* Host online webinars or demo sessions
* Mentor new contributors and help them get started
### [Hackathons](#hackathons)
[Section titled “Hackathons”](#hackathons)
* Participate in existing hackathons using Rafiki
* Develop innovative applications and integrations
* Create proof-of-concept projects that showcase Rafiki’s capabilities
* Collaborate with others on experimental features
## [🤝 Getting help](#-getting-help)
[Section titled “🤝 Getting help”](#-getting-help)
Stuck on something? We’re here to help:
* Ask questions in the `#rafiki` Slack channel
* Start a discussion on GitHub for broader topics
* Join our [community calls](https://calendar.google.com/calendar/event?action=TEMPLATE\&tmeid=Z2trYXZsaG1hNWJxZGptbXRzdTlna2p0NXRfMjAyNTA3MDhUMTQzMDAwWiBtb2hhbW1lZEBpbnRlcmxlZGdlci5vcmc\&tmsrc=mohammed%40interledger.org\&scp=ALL)
Remember, every contribution matters - whether it’s a small documentation fix, a bug report, or a major feature implementation. We’re excited to have you as part of the Rafiki community!
# Glossary
## [Account servicing entity (ASE)](#account-servicing-entity-ase)
[Section titled “Account servicing entity (ASE)”](#account-servicing-entity-ase)
An entity that provides and maintains a payment account for a payer and/or payee. An ASE is a regulated entity within the country or countries it operates. Examples include digital wallets, banks, and mobile money providers. Non-regulated entities should not use Rafiki in production environments due to the potential legal and compliance risks involved.
## [Auth service](#auth-service)
[Section titled “Auth service”](#auth-service)
A reference implementation of an Open Payments authorization server in Rafiki. The `auth` service manages grant authorization and authentication, allowing clients (for example, third-party applications) to create payments and quotes. It issues access tokens and validates client access rights through communication with the resource server.
## [Authorization server](#authorization-server)
[Section titled “Authorization server”](#authorization-server)
A server that grants delegated authorization and privileges, via [GNAP](#grant-negotiation-and-authorization-protocol-gnap), to a particular instance of client software in the form of access tokens, allowing the client to call the Open Payments APIs.
We’ve provided an opinionated version of a GNAP authorization server via the `auth` service, meaning that we’ve made certain decisions regarding the implementation and configuration of the server that may limit customization but ensure consistency and adherence to preferred practices.
## [Backend service](#backend-service)
[Section titled “Backend service”](#backend-service)
The core service in Rafiki responsible for managing business logic and external communication. The `backend` service exposes Open Payments API endpoints for account management, operates an Interledger connector for STREAM packet exchange, and provides a GraphQL Backend Admin API for managing accounts and settings.
## [Client](#client)
[Section titled “Client”](#client)
An application or service, such as a mobile or web app, that interacts with the authorization server to obtain grants and access tokens. Clients use tokens to access resource servers and perform actions, such as retrieving transaction history and setting up payments, on behalf of a user or system.
## [Frontend service](#frontend-service)
[Section titled “Frontend service”](#frontend-service)
An optional internal interface in Rafiki, known as the Rafiki Admin, used to manage your Rafiki instance. The `frontend` service communicates with the Backend Admin API through a Remix web app, facilitating administrative tasks within the Rafiki environment.
## [Grant Negotiation and Authorization Protocol (GNAP)](#grant-negotiation-and-authorization-protocol-gnap)
[Section titled “Grant Negotiation and Authorization Protocol (GNAP)”](#grant-negotiation-and-authorization-protocol-gnap)
The Grant Negotiation Authorization Protocol (GNAP) defines a mechanism for delegating authorization to a piece of software (client), and conveying the results and artifacts of that delegation to the software. This delegation can include access to a set of APIs as well as subject information passed directly to the software. For more information, refer to the [GNAP specification](https://datatracker.ietf.org/doc/html/draft-ietf-gnap-core-protocol-12).
## [Grant](#grant)
[Section titled “Grant”](#grant)
A delegation of authorization from a resource owner to a client, allowing the client to access protected resources or perform actions on the owner’s behalf. In Rafiki, this process is managed by the authorization server, which issues grants as access tokens. These grants permit clients to interact with Open Payments APIs to, for example, create payments and retrieve account information, based on the permissions granted by the resource owner.
## [Identity provider (IdP)](#identity-provider-idp)
[Section titled “Identity provider (IdP)”](#identity-provider-idp)
A system or service that stores and manages user identity information, authentication, and consent. Due to Rafiki’s implementation of the Open Payments standard, Rafiki requires integration with an IdP to support interactive Open Payments grants.
## [Incoming payment](#incoming-payment)
[Section titled “Incoming payment”](#incoming-payment)
An object created by the recipient’s ASE, on their resource server, that represents a payment being received. The object contains information about the incoming payment, such as the amount, currency, receiver’s wallet address, and payment status. The object is used to track and manage payments that are expected to be or have been received.
## [Interledger Protocol (ILP)](#interledger-protocol-ilp)
[Section titled “Interledger Protocol (ILP)”](#interledger-protocol-ilp)
An open protocol stack designed to facilitate the transfer of value across different currencies, platforms, and payment networks. Rafiki is a reference implementation of the Interledger stack, allowing you to join the Interledger network and quickly enable Interledger capabilities on your users’ accounts. For more information, refer to the [Interledger specification](https://interledger.org/developers/get-started/).
## [ILP packet](#ilp-packet)
[Section titled “ILP packet”](#ilp-packet)
A unit of data that carries payment information through the Interledger network. A single payment can be broken into smaller packets of value which are then routed across the network.
## [Open Payments](#open-payments)
[Section titled “Open Payments”](#open-payments)
An API standard and a set of APIs that allows clients to securely retrieve account information and initiate payments from your customers’ accounts with their consent. By adhering to this standard, Rafiki enables integration with external applications and supports the secure and uniform management of payments, quotes, and account data through the Open Payments APIs. For more information, visit the [Open Payments documentation](https://openpayments.dev).
## [Outgoing payment](#outgoing-payment)
[Section titled “Outgoing payment”](#outgoing-payment)
An object created by the sender’s ASE, on their resource server, that represents a payment being sent. This object contains information about the outgoing payment, such as the amount, currency, receiver’s wallet address, and payment status.
## [Payment pointer](#payment-pointer)
[Section titled “Payment pointer”](#payment-pointer)
A type of wallet address that serves as an SPSP endpoint to facilitate sending and receiving ILP packets. Payment pointers can be written out using the `$` shorthand (for example, `$wallet.example.com/alice`) or as a URL (for example, `https://wallet.example.com/alice`).
## [Peer](#peer)
[Section titled “Peer”](#peer)
A counterparty with whom you transact with over the Interledger network. Your Rafiki instance holds liquidity accounts for each of your peers.
## [Quote](#quote)
[Section titled “Quote”](#quote)
An object created by the sender’s ASE, on their resource server, that represents the total cost for the sender to send a payment. When a quote is created, it serves as a commitment from the sender’s ASE to deliver the amount to the recipient’s ASE and is only valid for a limited time.
## [Resource server](#resource-server)
[Section titled “Resource server”](#resource-server)
A server that hosts and manages access to protected Open Payments resources for incoming payments, quotes, and outgoing payments.
Rafiki’s `backend` service runs an Open Payments resource server.
## [Simple Payment Setup Protocol (SPSP)](#simple-payment-setup-protocol-spsp)
[Section titled “Simple Payment Setup Protocol (SPSP)”](#simple-payment-setup-protocol-spsp)
An Interledger application layer protocol for exchanging payment information between two counterparties to facilitate direct payments over Interledger. The information is then used to set up a STREAM connection. You can read more about SPSP in its [specification](https://interledger.org/developers/rfcs/simple-payment-setup-protocol/).
## [Streaming Transport for the Real-Time Exchange of Assets and Messages (STREAM)](#streaming-transport-for-the-real-time-exchange-of-assets-and-messages-stream)
[Section titled “Streaming Transport for the Real-Time Exchange of Assets and Messages (STREAM)”](#streaming-transport-for-the-real-time-exchange-of-assets-and-messages-stream)
An Interledger transport layer protocol for sending and receiving authenticated ILP packets between peers and determining the path exchange rate. See the [STREAM specification](https://interledger.org/developers/rfcs/stream-protocol/) for more information.
## [Wallet address](#wallet-address)
[Section titled “Wallet address”](#wallet-address)
A secure, unique URL that identifies an Open Payments-enabled account. It acts as an entry point to the Open Payments APIs, facilitating interactions like sending and receiving payments. Similar to how an email address serves as a public identifier for an email account, a wallet address is publicly shareable and used to interact with the underlying payment account without compromising its security. Wallet address URLs are treated as case-insensitive, meaning that both lowercase and uppercase variations of the same address will be recognized as identical.
## [Web Monetization](#web-monetization)
[Section titled “Web Monetization”](#web-monetization)
A browser API that allows websites to signal their ability to receive Web Monetization payments from their site visitors. Rafiki supports Web Monetization payment flows natively, enabling seamless integration for websites looking to implement the feature and monetize their content via the Interledger network. For more details, visit the [Web Monetization](https://webmonetization.org/) website.
# Releases
## [Release notes](#release-notes)
[Section titled “Release notes”](#release-notes)
Refer to the Rafiki releases [page](https://github.com/interledger/rafiki/releases) for the latest release notes.
## [Rafiki packages](#rafiki-packages)
[Section titled “Rafiki packages”](#rafiki-packages)
The latest sources for each of the Rafiki [services](/v1-beta/resources/architecture) are available through GitHub.
* [Backend](https://github.com/interledger/rafiki/tree/main/packages/backend)
* [Auth](https://github.com/interledger/rafiki/tree/main/packages/auth)
* [Frontend](https://github.com/interledger/rafiki/tree/main/packages/frontend)
Alternatively, if you want to run the latest Rafiki version using Docker Compose, then refer to this [guide](/v1-beta/integration/deployment/docker-compose).
# Webhook event types
Webhooks notify you of specific events that occur within your Rafiki instance, allowing you to integrate Interledger payments with your system and business processes. For example, Rafiki can notify you when one of your account holders has received an Interledger payment, at which point you would credit their account on your ledger.
The following is an enumeration of all of Rafiki’s [webhook event](/v1-beta/integration/requirements/webhook-events) types along with their descriptions, which you must listen to and handle.
| Value | Description |
| ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| [`incoming_payment.created`](/v1-beta/integration/requirements/webhook-events/#incoming-payment-created) | An incoming payment was created. |
| [`incoming_payment.completed`](/v1-beta/integration/requirements/webhook-events/#incoming-payment-completed) | An incoming payment is complete and will not accept any additional incoming funds. |
| [`incoming_payment.expired`](/v1-beta/integration/requirements/webhook-events/#incoming-payment-expired) | An incoming payment expired and will not accept any additional incoming funds. |
| [`outgoing_payment.created`](/v1-beta/integration/requirements/webhook-events/#outgoing-payment-created) | An outgoing payment was created. |
| [`outgoing_payment.completed`](/v1-beta/integration/requirements/webhook-events/#outgoing-payment-completed) | An outgoing payment completed. |
| [`outgoing_payment.failed`](/v1-beta/integration/requirements/webhook-events/#outgoing-payment-failed) | An outgoing payment partially or completely failed. |
| [`wallet_address.not_found`](/v1-beta/integration/requirements/webhook-events/#wallet-address-not-found) | A requested wallet address was not found. |
| [`wallet_address.web_monetization`](/v1-beta/integration/requirements/webhook-events/#wallet-address-web-monetization) | Web Monetization payments received via STREAM. |
| [`asset.liquidity_low`](/v1-beta/integration/requirements/webhook-events/#asset-liquidity-low) | Asset liquidity has dropped below defined threshold. |
| [`peer.liquidity_low`](/v1-beta/integration/requirements/webhook-events/#peer-liquidity-low) | Peer liquidity has dropped below defined threshold. |