Skip to content
GitHub

Webhook Events

Paired together with the backend Admin API, webhook events are the main communication channel between the Rafiki instance and the Account Servicing Entity. Most events require the Account Servicing Entity’s server to interact with Rafiki, either to deposit or withdraw liquidity into or from Rafiki, or to provide wallet address information. This document will describe how an Account Servicing Entity should handle each of the defined webhook events.

Setup

When events happen in the Rafiki instance, the Rafiki backend service will make a POST request to the configured WEBHOOK_URL (backend environment variable), and will expect a 200 status in the response.

Request Body

Each webhook has the following structure in the request body:

Variable NameTypeDescription
idStringUnique UUID
typeEnum: EventType
dataObjectEvent data

EventType

ValueDescription
incoming_payment.createdAn incoming payment has been created.
incoming_payment.completedAn incoming payment is complete and will not accept any additional incoming funds.
incoming_payment.expiredAn incoming payment expired and will not accept any additional incoming funds.
outgoing_payment.createdAn outgoing payment was created.
outgoing_payment.completedAn outgoing payment completed.
outgoing_payment.failedAn outgoing payment partially or completely failed.
wallet_address.not_foundA requested wallet address was not found.
wallet_address.web_monetizationWeb Monetization payments received via STREAM.
asset.liquidity_lowAsset liquidity has dropped below defined threshold.
peer.liquidity_lowPeer liquidity has dropped below defined threshold.

An OpenAPI specification of the webhook requests and their corresponding data can be found here.

Additionally, the local playground contains example payloads in the Bruno collection that can be used to test a webhook service integration.

Events

incoming_payment.created

The incoming_payment.created event indicates that an incoming payment has been created. This webhook event is only informational, since at this point, the incoming payment has not received any funds (i.e. no actions around liquidity are required). This webhook event could be used by the Account Servicing Entity to display upcoming incoming payments to their users.

sequenceDiagram
    participant ASE as Account Servicing Entity
    participant R as Rafiki

    R->>ASE: webhook event: incoming payment created
    ASE->>ASE: no action required

incoming_payment.completed

The incoming_payment.completed event indicates that an incoming payment has been completed, either automatically or manually, and that any funds that have been received into this incoming payment should be withdrawn and credited to the recipient’s account with the Account Servicing Entity.

In addition, the CreateIncomingPaymentWithdrawal mutation supports two-phase transfers.

Example: An incoming payment was completed and received $10.

sequenceDiagram
    participant ASE as Account Servicing Entity
    participant R as Rafiki

    R->>ASE: webhook event: incoming payment completed,
receivedAmount: $10 ASE->>R: admin API call: CreateIncomingPaymentWithdrawal ASE->>ASE: credit receiver's account with $10

Example: An incoming payment supporting two-phase transfers was completed and recieved $10.

sequenceDiagram
    participant ASE as Account Servicing Entity
    participant R as Rafiki

      R->>ASE: webhook event: incoming payment completed,
receivedAmount: $10 ASE->>R: admin API call: CreateIncomingPaymentWithdrawal ASE->>ASE: credit receiver's account with $10 ASE->>R: admin API call: PostLiquidityWithdrawal R->>R: 2-phase transfer completed

incoming_payment.expired

The incoming_payment.expired event indicates that an incoming payment has expired, and that any funds that have been received into this incoming payment should be withdrawn and credited to the recipient’s account with the Account Servicing Entity. Note that this event is not fired if there were no funds received by the incoming payment since there is no action required by the Account Servicing Entity.

Example: An incoming payment has expired and received $2.55.

sequenceDiagram
    participant ASE as Account Servicing Entity
    participant R as Rafiki

    R->>ASE: webhook event: incoming payment expired,
receivedAmount: $2.55 ASE->>R: admin API call: CreateIncomingPaymentWithdrawal ASE->>ASE: credit receiver's account with $2.55

outgoing_payment.created

The outgoing_payment.created event indicates that an outgoing payment has been created and is awaiting liquidity. The Account Servicing Entity should verify the user account balance (and perform any other kinds of checks necessary) before funding or cancelling the outgoing payment. In the case where the outgoing payment should not be fullfilled, the Account Servicing Entity can cancel the outgoing payment. Otherwise, the Account Servicing Entity should put a hold on the sender’s account and deposit the funds into Rafiki.

Example: An outgoing payment for $12 has been created.

sequenceDiagram
    participant ASE as Account Servicing Entity
    participant R as Rafiki
    R->>ASE: webhook event: outgoing payment created,
debitAmount: $12 ASE->>ASE: check if account has enough balance alt Account has enough balance ASE->>ASE: put hold of $12 on sender's account ASE->>R: admin API call: DepositOutgoingPaymentLiquidity end alt Account does not have enough balance ASE->>R: admin API call: CancelOutgoingPayment
reason: Not enough balance end

outgoing_payment.completed

The outgoing_payment.completed event indicates that an outgoing payment has successfully sent as many funds as possible to the receiver. The Account Servicing Entity should withdraw any excess liquidity from that outgoing payment in Rafiki and use it as they see fit. One option would be to return it to the sender. Another option is that the excess liquidity is considered a fee and retained by the Account Servicing Entity. Furthermore, the Account Servicing Entity should remove the hold on the sender’s account and debit it.

In addition, the CreateOutgoingPaymentWithdrawal mutation supports two-phase transfers.

Example: An outgoing payment amount for $12 has been completed. $11.50 were sent. The Account Servicing Entity keeps $0.50 as fees.

sequenceDiagram
    participant ASE as Account Servicing Entity
    participant R as Rafiki

    R->>ASE: webhook event: outgoing completed,
debitAmount: $12, sentAmount:$11.50 ASE->>R: admin API call: CreateOutgoingPaymentWithdrawal ASE->>ASE: remove the hold and deduct $12 from the sender's account,
credit ASE's account with $0.50

Example: An outgoing payment supporting two-phase transfers for $12 has been completed. $11.50 were sent. The Account Servicing Entity keeps $0.50 as fees.

sequenceDiagram
    participant ASE as Account Servicing Entity
    participant R as Rafiki

        R->>ASE: webhook event: outgoing completed,
debitAmount: $12, sentAmount:$11.50 ASE->>R: admin API call: CreateOutgoingPaymentWithdrawal ASE->>ASE: remove the hold and deduct $12 from the sender's account,
credit ASE's account with $0.50 ASE->>R: admin API call: PostLiquidityWithdrawal R->>R: 2-phase transfer completed

outgoing_payment.failed

The outgoing_payment.failed event indicates that an outgoing payment has either partially or completely failed and a retry was also unsuccessful. The Account Servicing Entity should withdraw any remaining liquidity from that outgoing payment in Rafiki. If the payment failed completely (the sentAmount was 0), the Account Servicing Entity should remove the hold from the sender’s account. If the payment failed partially, the Account Servicing Entity should remove the hold from the sender’s account and debit it with the amount that has been sent, but they should refrain from taking a sending fee.

Example: An outgoing payment for $12 has failed. $8 were sent.

sequenceDiagram
    participant ASE as Account Servicing Entity
    participant R as Rafiki

    R->>ASE: webhook event: outgoing failed,
debitAmount: $12, sentAmount:$8 ASE->>R: admin API call: CreateOutgoingPaymentWithdrawal ASE->>ASE: remove the hold and deduct $8 from the sender's account

wallet_address.web_monetization

The wallet_address.web_monetization event indicates that a wallet address has received web monetization payments via STREAM (raw ILP access). The Account Servicing Entity should withdraw that liquidity from the wallet address and credit the receiver’s account.

Example: A wallet address received $0.33

sequenceDiagram
    participant ASE as Account Servicing Entity
    participant R as Rafiki

    R->>ASE: webhook event: wallet address web monetization,
receivedAmount: $0.33 ASE->>R: admin API call: CreateWalletAddressWithdrawal ASE->>ASE: credit receiver's account with $0.33

wallet_address.not_found

The wallet_address.not_found event indicates that a wallet address was requested (via the Open Payments API), but it doesn’t exist in Rafiki. When receiving this event, the Account Servicing Entity can perform a lookup for the relevant account in their system, and create a wallet address. The initial request for the wallet address will succeed if the Account Servicing Entity creates it within the configurable WALLET_ADDRESS_LOOKUP_TIMEOUT_MS timeframe.

Example: The wallet address https://example-wallet.com/carla_garcia was requested but does not yet exist

sequenceDiagram
    participant ASE as Account Servicing Entity
    participant R as Rafiki

    R->>ASE: webhook event: wallet address not found,
wallet address: https://example-wallet.com/carla_garcia ASE->>R: admin API call: CreateWalletAddress
url: https://example-wallet.com/carla_garcia,
public name: Carla Eva Garcia

asset.liquidity_low

The asset.liquidity_low event indicates that the liquidity of an asset has dropped below a predefined liquidity threshold. When receiving this event, the Account Servicing Entity should check if they have or can acquire additional liquidity of said asset and if so, deposit it in Rafiki. If the Account Servicing Entity cannot or does not increase the asset liquidity in Rafiki, cross-currency transfers will fail.

Example: The asset liquidity for USD (scale: 2) drops below 100.00 USD.

sequenceDiagram
    participant ASE as Account Servicing Entity
    participant R as Rafiki

    R->>ASE: webhook event: liquidity (asset) low,
asset: USD (scale: 2, id: "abc") ASE->>R: admin API call: DepositAssetLiquidity

peer.liquidity_low

The peer.liquidity_low event indicates that the liquidity of a peer has dropped below a predefined liquidity threshold. When receiving this event, the Account Servicing Entity need to decide if they can extend said peer’s credit line or whether they need to settle first and then extend a new line of credit. If the Account Servicing Entity cannot or does not increase the peer liquidity in Rafiki, transfers to that peer will fail.

Example: The peer liquidity for Happy Life Bank drops below 100.00 USD.

sequenceDiagram
    participant ASE as Account Servicing Entity
    participant R as Rafiki

    R->>ASE: webhook event: liquidity (peer) low,
peer: Happy Life Bank (asset: "USD", scale: 2, id: "abc") ASE->>R: admin API call: DepositPeerLiquidity

Errors

If an error occurs when Rafiki sends a webhook event (a non-200 status is returned or the request times out), Rafiki will keep retrying the webhook requests at increasing intervals (first retry after 10 seconds, second after 20 more, next after 30 more, etc.) until a 200 status is returned. The maximum number of retries for webhook events can be configured via the WEBHOOK_MAX_RETRY environment variable.

Additionally, the timeout for webhook requests can be configured with the WEBHOOK_TIMEOUT environment variable.

Best Practices

Duplicate Events

The id in the webhook event payload is a unique UUID which can be used by the Account Servicing Entity’s system to determine whether the event has been received previously, preventing duplicate processing of events.

Asynchronous Handling

Account Servicing Entities should consider using a worker to process received webhook events, especially if requests to credit/debit user accounts are long processes. This would allow the server to process events at a rate suitable for the system, and would reduce the amount of failed/retried webhook events, since the webhook event listener can immediately reply with a successful 200 status.