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.
Variable | Type | Description |
---|---|---|
WEBHOOK_URL | backend | The 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.
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 |
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
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 isv1
.
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
, anddata
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
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.
Variable | Type | Description |
---|---|---|
WEBHOOK_TIMEOUT | backend | The amount of time, in milliseconds, after which a webhook request will time 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
Incoming payments
Event type | Description |
---|---|
incoming_payment.created | An incoming payment has been created |
incoming_payment.completed | An incoming payment is complete and will not accept any additional incoming funds |
incoming_payment.expired | An 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 type | Description |
---|---|
outgoing_payment.created | An outgoing payment has been created |
outgoing_payment.completed | An outgoing payment has completed |
outgoing_payment.failed | An 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 type | Description |
---|---|
wallet_address.not_found | The requested wallet address was not found on this Rafiki instance |
wallet_address.web_monetization | Web 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 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
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 type | Description |
---|---|
asset.liquidity_low | Your 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 type | Description |
---|---|
peer.liquidity_low | Your 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.