- Understanding how to set up your ledger for effective tracking of deposits and payouts in your system.
- Designing flexible workflows to accommodate various scenarios.
- Ensuring reliable and accurate transaction processing for your users.
Prerequisites
Before starting, ensure you have:- A running Blnk Core instance (e.g. at
http://localhost:5001). - An API key for Blnk (replace
YOUR_API_KEYin the code examples). Required for authenticated requests. - Optionally, you can connect your Blnk Core to your Blnk Cloud workspace to view your ledger data.
Handling deposits
When using Blnk to manage deposits, you can track the sources of your deposits-such as Stripe, Acme Bank, or others-directly in your ledger, enabling more detailed and accurate reporting. As illustrated in the map below, each source can be assigned to an internal balance, which will be reflected as the value of thesource field in your transaction request. Fees (1% of the deposit amount, capped at $5) are deducted and tracked separately.

- The source of the deposit, for example, @AcmeBank if it originates from Acme Bank.
- That the customer receives funds after the fees have been dededucted.
- The deposit fees are tracked using the
@Feesbalance.
Deposits with fee processing
When recording a deposit with fee processing, we’ll use the Multiple Destinations feature to distribute the incoming funds between the customer’s balance and the fees balance. We’ll also applyinflight to validate the deposits against predefined business rules before finalizing the transaction.
curl -X POST "http://localhost:5001/transactions" \
-H "X-blnk-key: <api-key>" \
-H "Content-Type: application/json" \
-d '{
"precise_amount": 10000,
"precision": 100,
"reference": "stripe_payment_ref_001",
"currency": "USD",
"source": "@Stripe",
"inflight": true,
"skip_queue": true,
"destinations": [
{
"identifier": "bln_CUSTOMER_BALANCE_ID",
"distribution": "99.5",
"narration": "Deposit to your account"
},
{
"identifier": "@Fees",
"distribution": "0.5",
"narration": "Processing fee"
}
],
"meta_data": {
"fee_amount": 0.5,
"stripe_payment_id": "pi_123456"
}
}'
async function recordDeposit(sourceId, customerBalanceId, amount, reference, stripePaymentId) {
// Calculate fee (1% capped at $5)
const feePercentage = 0.01;
const feeCap = 5;
const calculatedFee = Math.min(amount * feePercentage, feeCap);
const customerAmount = amount - calculatedFee;
// Record the deposit with multiple destinations
const transaction = await blnk.Transactions.create({
precise_amount: amount * 100, // Total amount being deposited
precision: 100,
reference: reference,
currency: "USD",
source: sourceId,
inflight: true, // Hold for verification
destinations: [
{
identifier: customerBalanceId,
distribution: `${customerAmount}`,
narration: "Deposit to your account"
},
{
identifier: "@Fees",
distribution: `${calculatedFee}`,
narration: "Processing fee"
}
],
meta_data: {
fee_amount: calculatedFee,
stripe_payment_id: stripePaymentId
},
skip_queue: true // Process immediately, bypassing queue
});
console.log(`Deposit recorded with ID: ${transaction.data.transaction_id}`);
return transaction.data;
}
func recordDeposit(sourceID, customerBalanceID string, amount float64, reference, stripePaymentID string) (*blnkgo.Transaction, error) {
client := getClient()
// Calculate fee (1% capped at $5)
feePercentage := 0.01
feeCap := 5.0
calculatedFee := math.Min(amount*feePercentage, feeCap)
customerAmount := amount - calculatedFee
// Record the deposit with multiple destinations
transaction, _, err := client.Transaction.Create(blnkgo.CreateTransactionRequest{
ParentTransaction: blnkgo.ParentTransaction{
PreciseAmount: big.NewInt(int64(amount * 100)),
Precision: 100,
Reference: reference,
Currency: "USD",
Source: sourceID,
SkipQueue: true,
Destinations: []blnkgo.Source{
{
Identifier: customerBalanceID,
Distribution: fmt.Sprintf("%g", customerAmount),
},
{
Identifier: "@Fees",
Distribution: fmt.Sprintf("%g", calculatedFee),
},
},
MetaData: blnkgo.MetaData{
"fee_amount": calculatedFee,
"stripe_payment_id": stripePaymentID,
},
},
Inflight: true,
})
if err != nil {
return nil, err
}
fmt.Printf("Deposit recorded with ID: %s\n", transaction.TransactionID)
return transaction, nil
}
def record_deposit(source_id, customer_balance_id, amount, reference, stripe_payment_id):
# Calculate fee (1% capped at $5)
fee_percentage = 0.01
fee_cap = 5
calculated_fee = min(amount * fee_percentage, fee_cap)
customer_amount = amount - calculated_fee
# Record the deposit with multiple destinations
transaction = blnk.transactions.create({
"precise_amount": amount * 100,
"precision": 100,
"reference": reference,
"currency": "USD",
"source": source_id,
"inflight": True,
"destinations": [
{
"identifier": customer_balance_id,
"distribution": f"{customer_amount}",
"narration": "Deposit to your account",
},
{
"identifier": "@Fees",
"distribution": f"{calculated_fee}",
"narration": "Processing fee",
},
],
"meta_data": {
"fee_amount": calculated_fee,
"stripe_payment_id": stripe_payment_id,
},
"skip_queue": True,
})
print(f"Deposit recorded with ID: {transaction.data['transaction_id']}")
return transaction.data
When you apply
inflight to a multiple destinations transaction, all associated transactions-distributing funds to both destinations-are also placed in an inflight state until the commit or void is executed.Verifying deposits based on business rules
Next, we’ll develop functions to process deposits according to the following business rules. In this tutorial, we’ll implement checks for two rules:- If the balance is blocked or frozen, the deposit should be voided.
-
If the deposit amount exceeds 1 million USD, it should remain in an
inflightstate, and a notification should be sent for further investigation.
Rule 1: Check if balance is blocked or frozen
# Get transaction details
curl -X GET "http://localhost:5001/transactions/{transaction_id}" \
-H "X-blnk-key: <api-key>"
# Get customer balance metadata
curl -X GET "http://localhost:5001/balances/{balance_id}" \
-H "X-blnk-key: <api-key>"
# Add void reason metadata
curl -X POST "http://localhost:5001/{transaction_id}/metadata" \
-H "X-blnk-key: <api-key>" \
-H "Content-Type: application/json" \
-d '{
"meta_data": {
"void_reason": "Account frozen"
}
}'
# Void inflight transaction if account is frozen/blocked
curl -X PUT "http://localhost:5001/transactions/inflight/{transaction_id}" \
-H "X-blnk-key: <api-key>" \
-H "Content-Type: application/json" \
-d '{
"status": "void"
}'
async function processDepositBasedOnRules(transactionId) {
const baseUrl = process.env.BLNK_BASE_URL ?? 'http://localhost:5001';
const apiKey = process.env.BLNK_API_KEY ?? '';
const txnRes = await fetch(`${baseUrl}/transactions/${transactionId}`, {
headers: { 'X-Blnk-Key': apiKey },
});
if (!txnRes.ok) throw new Error(await txnRes.text());
const transaction = await txnRes.json();
const destinations = transaction.destinations || [];
const customerBalanceId = destinations[0]?.identifier;
const balanceResponse = await blnk.LedgerBalances.get(customerBalanceId);
const meta = balanceResponse.data?.meta_data || {};
if (meta.status === 'frozen' || meta.status === 'blocked') {
const metaUpdate = await fetch(`${baseUrl}/${transactionId}/metadata`, {
method: 'POST',
headers: {
'X-Blnk-Key': apiKey,
'Content-Type': 'application/json',
},
body: JSON.stringify({
meta_data: { void_reason: `Account ${meta.status}` },
}),
});
if (!metaUpdate.ok) throw new Error(await metaUpdate.text());
const voidResult = await blnk.Transactions.updateStatus(transactionId, {
status: 'void',
});
if (voidResult.status !== 200 || !voidResult.data) {
throw new Error(voidResult.message ?? 'Failed to void transaction');
}
console.log(`Transaction ${transactionId} voided: account ${meta.status}`);
return voidResult.data;
}
return await processDepositByAmount(transactionId);
}
func processDepositBasedOnRules(transactionID string) (*blnkgo.Transaction, error) {
client := getClient()
transaction, _, err := client.Transaction.Get(transactionID)
if err != nil {
return nil, err
}
var customerBalanceID string
if len(transaction.Destinations) > 0 {
customerBalanceID = transaction.Destinations[0].Identifier
}
balanceResponse, _, err := client.LedgerBalance.Get(customerBalanceID)
if err != nil {
return nil, err
}
status, _ := balanceResponse.MetaData["status"].(string)
if status == "frozen" || status == "blocked" {
_, _, err = client.Metadata.UpdateMetadata(transactionID, blnkgo.UpdateMetaDataRequest{
MetaData: blnkgo.MetaData{
"void_reason": fmt.Sprintf("Account %s", status),
},
})
if err != nil {
return nil, err
}
voidResult, _, err := client.Transaction.Update(transactionID, blnkgo.UpdateStatus{
Status: "void",
})
if err != nil {
return nil, err
}
fmt.Printf("Transaction %s voided: account %s\n", transactionID, status)
return voidResult, nil
}
return processDepositByAmount(transactionID)
}
def process_deposit_based_on_rules(transaction_id):
transaction = blnk.transactions.get(transaction_id)
transaction_data = transaction.data
destinations = transaction_data.get("destinations") or []
customer_balance_id = destinations[0]["identifier"] if destinations else None
balance_response = blnk.ledger_balances.get(customer_balance_id)
meta = balance_response.data.get("meta_data") or {}
if meta.get("status") in ("frozen", "blocked"):
blnk.metadata.update(transaction_id, {
"meta_data": {
"void_reason": f"Account {meta['status']}",
},
})
void_result = blnk.transactions.update_status(transaction_id, {
"status": "void",
})
if void_result.status != 200 or not void_result.data:
raise Exception(void_result.message or "Failed to void transaction")
print(f"Transaction {transaction_id} voided: account {meta['status']}")
return void_result.data
return process_deposit_by_amount(transaction_id)
Rule 2: Check amount rule
# Get transaction amount
curl -X GET "http://localhost:5001/transactions/{transaction_id}" \
-H "X-blnk-key: <api-key>"
# Commit if below threshold ($1M)
curl -X PUT "http://localhost:5001/transactions/inflight/{transaction_id}" \
-H "X-blnk-key: <api-key>" \
-H "Content-Type: application/json" \
-d '{
"status": "commit"
}'
# Or flag large deposits for investigation
curl -X POST "http://localhost:5001/{transaction_id}/metadata" \
-H "X-blnk-key: <api-key>" \
-H "Content-Type: application/json" \
-d '{
"meta_data": {
"investigation_required": true,
"investigation_reason": "Large deposit amount",
"flagged_at": "2024-12-21T01:36:46.997063436Z"
}
}'
async function processDepositByAmount(transactionId) {
const baseUrl = process.env.BLNK_BASE_URL ?? 'http://localhost:5001';
const apiKey = process.env.BLNK_API_KEY ?? '';
const txnRes = await fetch(`${baseUrl}/transactions/${transactionId}`, {
headers: { 'X-Blnk-Key': apiKey },
});
if (!txnRes.ok) throw new Error(await txnRes.text());
const transaction = await txnRes.json();
const amount = transaction.precise_amount;
if (amount < 100000000) {
const commit = await blnk.Transactions.updateStatus(transactionId, {
status: 'commit',
});
if (commit.status !== 200 || !commit.data) {
throw new Error(commit.message ?? 'Failed to commit transaction');
}
console.log(`Transaction ${transactionId} committed: amount below threshold`);
return commit.data;
}
const metaRes = await fetch(`${baseUrl}/${transactionId}/metadata`, {
method: 'POST',
headers: {
'X-Blnk-Key': apiKey,
'Content-Type': 'application/json',
},
body: JSON.stringify({
meta_data: {
investigation_required: true,
investigation_reason: 'Large deposit amount',
flagged_at: new Date().toISOString(),
},
}),
});
if (!metaRes.ok) throw new Error(await metaRes.text());
const flagged = await metaRes.json();
await triggerLargeDepositHook(transactionId, amount);
console.log(`Transaction ${transactionId} flagged for investigation: large amount`);
return flagged;
}
func processDepositByAmount(transactionID string) (interface{}, error) {
client := getClient()
transaction, _, err := client.Transaction.Get(transactionID)
if err != nil {
return nil, err
}
amount, _ := transaction.PreciseAmount.Int64()
if amount < 100000000 {
commit, _, err := client.Transaction.Update(transactionID, blnkgo.UpdateStatus{
Status: "commit",
})
if err != nil {
return nil, err
}
fmt.Printf("Transaction %s committed: amount below threshold\n", transactionID)
return commit, nil
}
flagged, _, err := client.Metadata.UpdateMetadata(transactionID, blnkgo.UpdateMetaDataRequest{
MetaData: blnkgo.MetaData{
"investigation_required": true,
"investigation_reason": "Large deposit amount",
"flagged_at": time.Now().UTC().Format(time.RFC3339),
},
})
if err != nil {
return nil, err
}
triggerLargeDepositHook(transactionID, amount)
fmt.Printf("Transaction %s flagged for investigation: large amount\n", transactionID)
return flagged, nil
}
from datetime import datetime
def process_deposit_by_amount(transaction_id):
transaction = blnk.transactions.get(transaction_id)
amount = transaction.data["precise_amount"]
if amount < 100000000:
commit = blnk.transactions.update_status(transaction_id, {
"status": "commit",
})
if commit.status != 200 or not commit.data:
raise Exception(commit.message or "Failed to commit transaction")
print(f"Transaction {transaction_id} committed: amount below threshold")
return commit.data
flagged = blnk.metadata.update(transaction_id, {
"meta_data": {
"investigation_required": True,
"investigation_reason": "Large deposit amount",
"flagged_at": datetime.utcnow().isoformat(),
},
})
trigger_large_deposit_hook(transaction_id, amount)
print(f"Transaction {transaction_id} flagged for investigation: large amount")
return flagged
Putting it all together
# Step 1: Record deposit with fee processing (see recordDeposit)
curl -X POST "http://localhost:5001/transactions" \
-H "X-blnk-key: <api-key>" \
-H "Content-Type: application/json" \
-d '{
"precise_amount": 10000,
"precision": 100,
"reference": "stripe_pi_123456",
"currency": "USD",
"source": "@Stripe",
"inflight": true,
"skip_queue": true,
"destinations": [
{"identifier": "bln_CUSTOMER_BALANCE_ID", "distribution": "99.5"},
{"identifier": "@Fees", "distribution": "0.5"}
]
}'
# Step 2: Apply business rules (see processDepositBasedOnRules)
curl -X GET "http://localhost:5001/transactions/{transaction_id}" \
-H "X-blnk-key: <api-key>"
async function handleStripeDeposit(stripePaymentId, customerBalanceId, amount) {
// Step 1: Record the deposit with fee processing
const reference = `stripe_${stripePaymentId}`;
const depositData = await recordDeposit(
"@Stripe",
customerBalanceId,
amount,
reference,
stripePaymentId
);
// Step 2: Process the deposit based on business rules
const result = await processDepositBasedOnRules(depositData.transaction_id);
return {
message: `Deposit processed with status: ${result.status}`
};
}
func handleStripeDeposit(stripePaymentID, customerBalanceID string, amount float64) (map[string]string, error) {
// Step 1: Record the deposit with fee processing
reference := fmt.Sprintf("stripe_%s", stripePaymentID)
depositData, err := recordDeposit(
"@Stripe",
customerBalanceID,
amount,
reference,
stripePaymentID,
)
if err != nil {
return nil, err
}
// Step 2: Process the deposit based on business rules
result, err := processDepositBasedOnRules(depositData.TransactionID)
if err != nil {
return nil, err
}
return map[string]string{
"message": fmt.Sprintf("Deposit processed with status: %s", result.Status),
}, nil
}
def handle_stripe_deposit(stripe_payment_id, customer_balance_id, amount):
# Step 1: Record the deposit with fee processing
reference = f"stripe_{stripe_payment_id}"
deposit_data = record_deposit(
"@Stripe",
customer_balance_id,
amount,
reference,
stripe_payment_id,
)
# Step 2: Process the deposit based on business rules
result = process_deposit_based_on_rules(deposit_data["transaction_id"])
return {
"message": f"Deposit processed with status: {result['status']}",
}
Refunding deposits
To process refunds in your ledger and reverse a deposit:curl -X POST "http://localhost:5001/refund-transaction/{transaction_id}" \
-H "X-blnk-key: <api-key>"
curl -X POST "http://localhost:5001/{refund_id}/metadata" \
-H "X-blnk-key: <api-key>" \
-H "Content-Type: application/json" \
-d '{
"meta_data": {
"refund_reason": "Customer requested refund"
}
}'
async function refundDeposit(transactionId, reason = 'Customer requested refund') {
// Process the refund
const refund = await blnk.Transactions.refund(transactionId);
const baseUrl = process.env.BLNK_BASE_URL ?? 'http://localhost:5001';
const apiKey = process.env.BLNK_API_KEY ?? '';
const metaRes = await fetch(`${baseUrl}/${refund.data.refund_id}/metadata`, {
method: 'POST',
headers: {
'X-Blnk-Key': apiKey,
'Content-Type': 'application/json',
},
body: JSON.stringify({
meta_data: { refund_reason: reason },
}),
});
if (!metaRes.ok) throw new Error(await metaRes.text());
console.log(`Deposit refunded: ${refund.data.refund_id}`);
return refund.data;
}
func refundDeposit(transactionID, reason string) (*blnkgo.Transaction, error) {
client := getClient()
if reason == "" {
reason = "Customer requested refund"
}
refund, _, err := client.Transaction.Refund(transactionID)
if err != nil {
return nil, err
}
_, _, err = client.Metadata.UpdateMetadata(refund.TransactionID, blnkgo.UpdateMetaDataRequest{
MetaData: blnkgo.MetaData{
"refund_reason": reason,
},
})
if err != nil {
return nil, err
}
fmt.Printf("Deposit refunded: %s\n", refund.TransactionID)
return refund, nil
}
def refund_deposit(transaction_id, reason="Customer requested refund"):
# Process the refund
refund = blnk.transactions.refund(transaction_id)
blnk.metadata.update(refund.data["refund_id"], {
"meta_data": {
"refund_reason": reason,
},
})
print(f"Deposit refunded: {refund.data['refund_id']}")
return refund.data
Handling payouts
Payouts function similarly to deposits but in reverse. TheCustomer Balance serves as the source, while the Payout Destination and @Fees act as the destinations. Include @Fees if the customer incurs fees for payment processing.

- The destination of the withdrawal.
- That the customer receives funds after the fees have been deducted.
- The deposit fees are tracked using the
@Feesbalance.
Path 1: Successful payouts
Here, the payout was successfully completed by your payout provider:curl -X POST "http://localhost:5001/transactions" \
-H "X-blnk-key: <api-key>" \
-H "Content-Type: application/json" \
-d '{
"precise_amount": 10100,
"precision": 100,
"reference": "payout_ref_001",
"description": "Payout for bills",
"currency": "USD",
"source": "bln_CUSTOMER_BALANCE_ID",
"inflight": true,
"inflight_expiry_date": "2024-12-22T01:36:46.997063436Z",
"skip_queue": true,
"destinations": [
{
"identifier": "@BillsPayment",
"distribution": "100",
"narration": "Payout for bills"
},
{
"identifier": "@Fees",
"distribution": "1",
"narration": "Payout processing fee"
}
],
"meta_data": {
"payout_status": "pending",
"fee_amount": 1
}
}'
// Initiate a payout with inflight
async function initiatePayout(customerBalanceId, billsAmount, reference) {
// Calculate fee (0.5% with $1 minimum)
const feePercentage = 0.005;
const minFee = 1;
const calculatedFee = Math.max(billsAmount * feePercentage, minFee);
const totalAmount = billsAmount + calculatedFee;
const payout = await blnk.Transactions.create({
precise_amount: totalAmount * 100,
precision: 100,
reference: reference,
description: "Payout for bills",
currency: "USD",
source: customerBalanceId,
destinations: [
{
identifier: "@BillsPayment",
distribution: `${billsAmount}`,
narration: "Payout for bills"
},
{
identifier: "@Fees",
distribution: `${calculatedFee}`,
narration: "Payout processing fee"
}
],
inflight: true,
inflight_expiry_date: getExpiryDate(24), // Expires if no response is gotten in 24 hours
meta_data: {
payout_status: "pending",
fee_amount: calculatedFee
},
skip_queue: true // Process immediately, bypassing queue
});
console.log(`Payout initiated: ${payout.data.transaction_id}`);
return payout.data;
}
// Generate expiry date (hours from now)
function getExpiryDate(hours) {
const date = new Date();
date.setHours(date.getHours() + hours);
return date.toISOString();
}
func initiatePayout(customerBalanceID string, billsAmount float64, reference string) (*blnkgo.Transaction, error) {
client := getClient()
// Calculate fee (0.5% with $1 minimum)
feePercentage := 0.005
minFee := 1.0
calculatedFee := math.Max(billsAmount*feePercentage, minFee)
totalAmount := billsAmount + calculatedFee
payout, _, err := client.Transaction.Create(blnkgo.CreateTransactionRequest{
ParentTransaction: blnkgo.ParentTransaction{
PreciseAmount: big.NewInt(int64(totalAmount * 100)),
Precision: 100,
Reference: reference,
Description: "Payout for bills",
Currency: "USD",
Source: customerBalanceID,
SkipQueue: true,
Destinations: []blnkgo.Source{
{
Identifier: "@BillsPayment",
Distribution: fmt.Sprintf("%g", billsAmount),
},
{
Identifier: "@Fees",
Distribution: fmt.Sprintf("%g", calculatedFee),
},
},
MetaData: blnkgo.MetaData{
"payout_status": "pending",
"fee_amount": calculatedFee,
},
},
Inflight: true,
InflightExpiryDate: getExpiryDate(24),
})
if err != nil {
return nil, err
}
fmt.Printf("Payout initiated: %s\n", payout.TransactionID)
return payout, nil
}
func getExpiryDate(hours int) string {
return time.Now().UTC().Add(time.Duration(hours) * time.Hour).Format(time.RFC3339)
}
from datetime import datetime, timedelta
def initiate_payout(customer_balance_id, bills_amount, reference):
# Calculate fee (0.5% with $1 minimum)
fee_percentage = 0.005
min_fee = 1
calculated_fee = max(bills_amount * fee_percentage, min_fee)
total_amount = bills_amount + calculated_fee
payout = blnk.transactions.create({
"precise_amount": total_amount * 100,
"precision": 100,
"reference": reference,
"description": "Payout for bills",
"currency": "USD",
"source": customer_balance_id,
"destinations": [
{
"identifier": "@BillsPayment",
"distribution": f"{bills_amount}",
"narration": "Payout for bills",
},
{
"identifier": "@Fees",
"distribution": f"{calculated_fee}",
"narration": "Payout processing fee",
},
],
"inflight": True,
"inflight_expiry_date": get_expiry_date(24),
"meta_data": {
"payout_status": "pending",
"fee_amount": calculated_fee,
},
"skip_queue": True,
})
print(f"Payout initiated: {payout.data['transaction_id']}")
return payout.data
def get_expiry_date(hours):
return (datetime.utcnow() + timedelta(hours=hours)).isoformat()
Commit based on feedback from payout provider
curl -X POST "http://localhost:5001/search/transactions" \
-H "X-blnk-key: <api-key>" \
-H "Content-Type: application/json" \
-d '{
"q": "payout_ref_001",
"query_by": "reference",
"filter_by": "status:=INFLIGHT"
}'
curl -X POST "http://localhost:5001/{transaction_id}/metadata" \
-H "X-blnk-key: <api-key>" \
-H "Content-Type: application/json" \
-d '{
"meta_data": {
"payout_status": "confirmed",
"provider_payout_id": "po_123456",
"confirmation_time": "2024-12-21T01:36:46.997063436Z"
}
}'
curl -X PUT "http://localhost:5001/transactions/inflight/{transaction_id}" \
-H "X-blnk-key: <api-key>" \
-H "Content-Type: application/json" \
-d '{
"status": "commit"
}'
// Webhook handler for payment processor notifications
async function handlePaymentWebhook(webhookBody) {
const {
event_type,
payout_id,
status,
transaction_reference
} = webhookBody;
if (event_type === 'payment.success') {
// Find the transaction by reference
const searchResult = await blnk.Search.search({
q: transaction_reference,
query_by: "reference",
filter_by: "status:=INFLIGHT"
}, "transactions");
// Get the parent transaction (transaction_id connecting both transactions together)
const transaction = searchResult.data.hits[0].document;
const transactionId = transaction.parent_transaction;
const baseUrl = process.env.BLNK_BASE_URL ?? 'http://localhost:5001';
const apiKey = process.env.BLNK_API_KEY ?? '';
const metaRes = await fetch(`${baseUrl}/${transactionId}/metadata`, {
method: 'POST',
headers: {
'X-Blnk-Key': apiKey,
'Content-Type': 'application/json',
},
body: JSON.stringify({
meta_data: {
payout_status: 'confirmed',
provider_payout_id: payout_id,
confirmation_time: new Date().toISOString(),
},
}),
});
if (!metaRes.ok) throw new Error(await metaRes.text());
const commit = await blnk.Transactions.updateStatus(transactionId, {
status: 'commit',
});
if (commit.status !== 200 || !commit.data) {
throw new Error(commit.message ?? 'Failed to commit payout');
}
console.log(`Payout confirmed and committed: ${transactionId}`);
return {
processed: true,
transaction_id: transactionId,
};
}
}
func handlePaymentWebhook(webhookBody map[string]interface{}) (map[string]interface{}, error) {
client := getClient()
eventType, _ := webhookBody["event_type"].(string)
payoutID, _ := webhookBody["payout_id"].(string)
transactionReference, _ := webhookBody["transaction_reference"].(string)
if eventType == "payment.success" {
searchResult, _, err := client.Search.SearchDocument(
blnkgo.SearchParams{
Q: transactionReference,
QueryBy: "reference",
FilterBy: "status:=INFLIGHT",
},
blnkgo.Transactions,
)
if err != nil {
return nil, err
}
transaction := searchResult.Hits[0].Document
transactionID := transaction.ParentTransaction
_, _, err = client.Metadata.UpdateMetadata(transactionID, blnkgo.UpdateMetaDataRequest{
MetaData: blnkgo.MetaData{
"payout_status": "confirmed",
"provider_payout_id": payoutID,
"confirmation_time": time.Now().UTC().Format(time.RFC3339),
},
})
if err != nil {
return nil, err
}
_, _, err = client.Transaction.Update(transactionID, blnkgo.UpdateStatus{
Status: "commit",
})
if err != nil {
return nil, err
}
fmt.Printf("Payout confirmed and committed: %s\n", transactionID)
return map[string]interface{}{
"processed": true,
"transaction_id": transactionID,
}, nil
}
return nil, nil
}
from datetime import datetime
def handle_payment_webhook(webhook_body):
event_type = webhook_body["event_type"]
payout_id = webhook_body["payout_id"]
transaction_reference = webhook_body["transaction_reference"]
if event_type == "payment.success":
search_result = blnk.search.search({
"q": transaction_reference,
"query_by": "reference",
"filter_by": "status:=INFLIGHT",
}, "transactions")
transaction = search_result.data["hits"][0]["document"]
transaction_id = transaction["parent_transaction"]
blnk.metadata.update(transaction_id, {
"meta_data": {
"payout_status": "confirmed",
"provider_payout_id": payout_id,
"confirmation_time": datetime.utcnow().isoformat(),
},
})
commit = blnk.transactions.update_status(transaction_id, {
"status": "commit",
})
if commit.status != 200 or not commit.data:
raise Exception(commit.message or "Failed to commit payout")
print(f"Payout confirmed and committed: {transaction_id}")
return {
"processed": True,
"transaction_id": transaction_id,
}
How multiple destinations work
When using Multiple Destinations in a transaction (like in our tutorial), Blnk automatically generates a separate transaction record for each distribution. These individual records are linked together with theparent_transaction parameter.
This structure organizes and connects all parts of a multi-destination transaction efficiently.
A standout feature of this system is how actions on the parent_transaction affect the entire group:
- Committing the
parent_transactionautomatically commits all associated distributions, confirming the full distribution. - Voiding the
parent_transactionautomatically voids all distributions, canceling the entire distribution in one step.
Path 2: Failed payouts
The payout is pending, but a failure message was received from the provider, resulting in the transaction being voided:curl -X POST "http://localhost:5001/search/transactions" \
-H "X-blnk-key: <api-key>" \
-H "Content-Type: application/json" \
-d '{
"q": "payout_ref_001",
"query_by": "reference",
"filter_by": "status:=INFLIGHT"
}'
curl -X POST "http://localhost:5001/{transaction_id}/metadata" \
-H "X-blnk-key: <api-key>" \
-H "Content-Type: application/json" \
-d '{
"meta_data": {
"payout_status": "failed",
"provider_payout_id": "po_123456",
"confirmation_time": "2024-12-21T01:36:46.997063436Z"
}
}'
curl -X PUT "http://localhost:5001/transactions/inflight/{transaction_id}" \
-H "X-blnk-key: <api-key>" \
-H "Content-Type: application/json" \
-d '{
"status": "void"
}'
async function handlePaymentWebhook(webhookBody) {
const {
event_type,
payout_id,
status,
transaction_reference
} = webhookBody;
if (event_type === 'payment.failed') {
// Find the transaction by reference
const searchResult = await blnk.Search.search({
q: transaction_reference,
query_by: "reference",
filter_by: "status:=INFLIGHT"
}, "transactions");
// Get the parent transaction (transaction_id connecting both transactions together)
const transaction = searchResult.data.hits[0].document;
const transactionId = transaction.parent_transaction;
const baseUrl = process.env.BLNK_BASE_URL ?? 'http://localhost:5001';
const apiKey = process.env.BLNK_API_KEY ?? '';
const metaRes = await fetch(`${baseUrl}/${transactionId}/metadata`, {
method: 'POST',
headers: {
'X-Blnk-Key': apiKey,
'Content-Type': 'application/json',
},
body: JSON.stringify({
meta_data: {
payout_status: 'failed',
provider_payout_id: payout_id,
confirmation_time: new Date().toISOString(),
},
}),
});
if (!metaRes.ok) throw new Error(await metaRes.text());
const voidResult = await blnk.Transactions.updateStatus(transactionId, {
status: 'void',
});
if (voidResult.status !== 200 || !voidResult.data) {
throw new Error(voidResult.message ?? 'Failed to void payout');
}
console.log(`Payout cancelled: ${transactionId}`);
return {
processed: true,
transaction_id: transactionId,
};
}
}
func handlePaymentWebhook(webhookBody map[string]interface{}) (map[string]interface{}, error) {
client := getClient()
eventType, _ := webhookBody["event_type"].(string)
payoutID, _ := webhookBody["payout_id"].(string)
transactionReference, _ := webhookBody["transaction_reference"].(string)
if eventType == "payment.failed" {
searchResult, _, err := client.Search.SearchDocument(
blnkgo.SearchParams{
Q: transactionReference,
QueryBy: "reference",
FilterBy: "status:=INFLIGHT",
},
blnkgo.Transactions,
)
if err != nil {
return nil, err
}
transaction := searchResult.Hits[0].Document
transactionID := transaction.ParentTransaction
_, _, err = client.Metadata.UpdateMetadata(transactionID, blnkgo.UpdateMetaDataRequest{
MetaData: blnkgo.MetaData{
"payout_status": "failed",
"provider_payout_id": payoutID,
"confirmation_time": time.Now().UTC().Format(time.RFC3339),
},
})
if err != nil {
return nil, err
}
_, _, err = client.Transaction.Update(transactionID, blnkgo.UpdateStatus{
Status: "void",
})
if err != nil {
return nil, err
}
fmt.Printf("Payout cancelled: %s\n", transactionID)
return map[string]interface{}{
"processed": true,
"transaction_id": transactionID,
}, nil
}
return nil, nil
}
from datetime import datetime
def handle_payment_webhook(webhook_body):
event_type = webhook_body["event_type"]
payout_id = webhook_body["payout_id"]
transaction_reference = webhook_body["transaction_reference"]
if event_type == "payment.failed":
search_result = blnk.search.search({
"q": transaction_reference,
"query_by": "reference",
"filter_by": "status:=INFLIGHT",
}, "transactions")
transaction = search_result.data["hits"][0]["document"]
transaction_id = transaction["parent_transaction"]
blnk.metadata.update(transaction_id, {
"meta_data": {
"payout_status": "failed",
"provider_payout_id": payout_id,
"confirmation_time": datetime.utcnow().isoformat(),
},
})
void_result = blnk.transactions.update_status(transaction_id, {
"status": "void",
})
if void_result.status != 200 or not void_result.data:
raise Exception(void_result.message or "Failed to void payout")
print(f"Payout cancelled: {transaction_id}")
return {
"processed": True,
"transaction_id": transaction_id,
}
Path 2: Successful payout but reversed
The payout was initially successful, with confirmation received from the provider, but the funds were reversed by the provider due to an inability to settle the transaction:curl -X POST "http://localhost:5001/refund-transaction/{transaction_id}" \
-H "X-blnk-key: <api-key>"
curl -X POST "http://localhost:5001/{refund_id}/metadata" \
-H "X-blnk-key: <api-key>" \
-H "Content-Type: application/json" \
-d '{
"meta_data": {
"refund_reason": "Can't complete payout"
}
}'
async function refundPayout(transactionId, reason = "Can't complete payout") {
// Process the refund
const refund = await blnk.Transactions.refund(transactionId);
const baseUrl = process.env.BLNK_BASE_URL ?? 'http://localhost:5001';
const apiKey = process.env.BLNK_API_KEY ?? '';
const metaRes = await fetch(`${baseUrl}/${refund.data.refund_id}/metadata`, {
method: 'POST',
headers: {
'X-Blnk-Key': apiKey,
'Content-Type': 'application/json',
},
body: JSON.stringify({
meta_data: { refund_reason: reason },
}),
});
if (!metaRes.ok) throw new Error(await metaRes.text());
console.log(`Payout refunded: ${refund.data.refund_id}`);
return refund.data;
}
func refundPayout(transactionID, reason string) (*blnkgo.Transaction, error) {
client := getClient()
if reason == "" {
reason = "Can't complete payout"
}
refund, _, err := client.Transaction.Refund(transactionID)
if err != nil {
return nil, err
}
_, _, err = client.Metadata.UpdateMetadata(refund.TransactionID, blnkgo.UpdateMetaDataRequest{
MetaData: blnkgo.MetaData{
"refund_reason": reason,
},
})
if err != nil {
return nil, err
}
fmt.Printf("Payout refunded: %s\n", refund.TransactionID)
return refund, nil
}
def refund_payout(transaction_id, reason="Can't complete payout"):
# Process the refund
refund = blnk.transactions.refund(transaction_id)
blnk.metadata.update(refund.data["refund_id"], {
"meta_data": {
"refund_reason": reason,
},
})
print(f"Payout refunded: {refund.data['refund_id']}")
return refund.data
Conclusion
You now have a basic deposits and payouts system powered by Blnk Ledger. It efficiently processes transactions while ensuring accurate records. The system is flexible and can be expanded with features like:- Automated handling of failed transactions
- Custom withdrawal limits and approval workflows
- Support for multiple funding sources
- Real-time transaction monitoring and alerts