Skip to content
GitHub

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.

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 service makes a POST request to your configured webhook endpoint. The backend service expects a 200 status in return.

VariableTypeDescription
WEBHOOK_URLbackendThe endpoint to where requests are made when a webhook event occurs

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 will vary depending on the event.

AttributeTypeDescriptionRequired
idStringUUID for the eventY
typeEnumThe EventTypeY
dataObjectAdditional data that coincides with the EventTypeY
{
"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"
}
}

We provide an OpenAPI specification for the webhook events fired by Rafiki.

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

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

The Rafiki-Signature header in each webhook request has the following format:

Rafiki-Signature header

Rafiki-Signature: t=<timestamp>, v<version>=<signature_digest>
  • t=<timestamp>: The UNIX timestamp (in seconds) when the signature was generated.
  • v<version>=<digest>: The versioned HMAC SHA-256 signature digest. The default version is v1.

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

Use HMAC SHA-256 with the SIGNATURE_SECRET environment variable as the key and the signed payload string as the message.

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

Below is an example in JavaScript to verify Rafiki’s webhook signature:

Verify webhook signature example

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

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

If a non-200 status is returned, indicating an error, or the request times out, Rafiki will retry the webhook request at increasing intervals until a 200 status is returned. You can configure

The first retry is after 10 seconds. Additional retries occur after 20 more seconds, then after 30 more seconds, and so on.

VariableTypeDescription
WEBHOOK_TIMEOUTbackendThe amount of time, in milliseconds, after which a webhook request will time out
WEBHOOK_MAX_RETRYbackendThe maximum number of retries for a webhook event when a non-200 status is returned or if the request timed out

Webhook events

Incoming payments

Event typeDescription
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

Incoming payment created

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 will either complete or expire.

Incoming payment completed

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 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 ASE->>ASE: Credit recipient's account with $10 ASE->>R: Backend Admin API call: PostLiquidityWithdrawal 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

$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 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, 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.

Outgoing payments

Event typeDescription
outgoing_payment.createdAn outgoing payment has been created
outgoing_payment.completedAn outgoing payment has completed
outgoing_payment.failedAn outgoing payment partially or completely failed

Outgoing payment created

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 else Account has insufficient funds ASE->>R: Backend Admin API call: CancelOutgoingPayment,
reason: Insufficient funds 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

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

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

Event typeDescription
wallet_address.not_foundThe requested wallet address was not found on this Rafiki instance
wallet_address.web_monetizationWeb Monetization payments have been received via STREAM

Wallet address not found

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

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 variableTypeDescription
WALLET_ADDRESS_LOOKUP_TIMEOUT_MSbackendThe time in milliseconds that you have to create a missing wallet address before the initial request times out

Wallet address Web Monetization

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

Event typeDescription
asset.liquidity_lowYour asset liquidity has dropped below your defined threshold

Asset liquidity low

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

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

Event typeDescription
peer.liquidity_lowYour peer liquidity has dropped below your defined threshold

Peer liquidity low

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

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.