- Create a customer savings wallet balance.
- Initiate a scheduled deposit transaction. For one-time funding: Wallet Funding Tutorial →.
- Update the savings frequency.
Designing your map
Before writing code, it’s crucial to design a money movement map that outlines how money moves in your system. This serves as the blueprint for your implementation. For our scheduled savings deposit workflow, here’s our map:
Set up your implementation
Based on our map, we’ll implement the following steps:- Initiate a scheduled transaction to
Savings Wallet - Monitor the date of the next scheduled deposit to ensure timely execution.
- Set up a new scheduled transaction after the current one has been successfully processed.
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.
- A customer savings ledger to organise the customers’ savings balances. Learn how: How to Create a Ledger →
Create customer savings balance
Create a balance with theledger_id of your Customer Savings Ledger to represent your customer savings wallet:
curl -X POST "http://localhost:5001/balances" \
-H "X-blnk-key: <api-key>" \
-H "Content-Type: application/json" \
-d '{
"ledger_id": "<customer-savings-ledger-id>",
"identity_id": "<customer-identity-id>",
"currency": "USD",
"meta_data": {
"wallet_type": "savings",
"schedule_savings": true,
"savings_frequency": "monthly",
"savings_amount": 10000,
"next_scheduled_savings": ""
}
}'
async function createSavingsWallet(savingsLedgerId, identityId, currency) {
const savingsWallet = await blnk.LedgerBalances.create({
ledger_id: savingsLedgerId,
identity_id: identityId,
currency: currency,
meta_data: {
wallet_type: "savings",
schedule_savings: true,
savings_frequency: "monthly",
savings_amount: 10000,
next_scheduled_savings: "",
},
});
console.log("Savings Wallet created:", savingsWallet.data.balance_id);
return savingsWallet.data.balance_id;
}
func createSavingsWallet(savingsLedgerID, identityID, currency string) (string, error) {
client := getClient()
balance, _, err := client.LedgerBalance.Create(
blnkgo.CreateLedgerBalanceRequest{
LedgerID: savingsLedgerID,
IdentityID: identityID,
Currency: currency,
MetaData: blnkgo.MetaData{
"wallet_type": "savings",
"schedule_savings": true,
"savings_frequency": "monthly",
"savings_amount": 10000,
"next_scheduled_savings": "",
},
},
)
if err != nil {
return "", err
}
fmt.Println("Savings Wallet created:", balance.BalanceID)
return balance.BalanceID, nil
}
savings_wallet = blnk.ledger_balances.create({
"ledger_id": savings_ledger_id,
"identity_id": identity_id,
"currency": currency,
"meta_data": {
"wallet_type": "savings",
"schedule_savings": True,
"savings_frequency": "monthly",
"savings_amount": 10000,
"next_scheduled_savings": "",
},
})
print("Savings Wallet created:", savings_wallet.data["balance_id"])
ApiResponse<JsonNode> savings_wallet = blnk.ledgerBalances().create(
CreateLedgerBalance.create()
.ledgerId(savings_ledger_id)
.identityId(identity_id)
.currency(currency)
.metaData(Map.of(
"wallet_type", "savings",
"schedule_savings", true,
"savings_frequency", "monthly",
"savings_amount", 10000,
"next_scheduled_savings", ""
))
);
System.out.println(
"Savings Wallet created: " + savings_wallet.data().get("balance_id").asText()
);
- This customer balance is subscribed to scheduled deposits.
- Schedule frequency is monthly.
- The next scheduled savings date can be monitored on the customer balance
Initiate scheduled transaction
Schedule a transaction on the ledger with thescheduled_for parameter:
curl -X POST "http://localhost:5001/transactions" \
-H "X-blnk-key: <api-key>" \
-H "Content-Type: application/json" \
-d '{
"precise_amount": 100000,
"precision": 100,
"reference": "SAV_0001",
"description": "Scheduled savings deposit",
"currency": "USD",
"source": "@WorldUSD",
"destination": "<savings-balance-id>",
"scheduled_for": "2025-12-25T02:42:19Z",
"meta_data": {
"transaction_type": "savings"
}
}'
async function createScheduledSavingsTransaction(
savingsBalanceId,
savingsAmount,
uniqueReference,
description,
scheduledDate,
) {
const scheduledSavings = await blnk.Transactions.create({
precise_amount: savingsAmount * 100,
precision: 100,
reference: uniqueReference,
description: description || "Scheduled savings deposit",
currency: "USD",
source: "@WorldUSD", // Represents external funding source
destination: savingsBalanceId,
scheduled_for: scheduledDate,
meta_data: {
transaction_type: "savings",
},
});
console.log(
"Savings successfully scheduled:",
scheduledSavings.data.transaction_id,
);
return scheduledSavings.data.transaction_id;
}
func createScheduledSavingsTransaction(
savingsBalanceID string,
savingsAmount float64,
uniqueReference, description string,
scheduledDate time.Time,
) (string, error) {
client := getClient()
if description == "" {
description = "Scheduled savings deposit"
}
scheduledSavings, _, err := client.Transaction.Create(
blnkgo.CreateTransactionRequest{
ParentTransaction: blnkgo.ParentTransaction{
PreciseAmount: savingsAmount * 100,
Precision: 100,
Reference: uniqueReference,
Description: description,
Currency: "USD",
Source: "@WorldUSD",
Destination: savingsBalanceID,
MetaData: blnkgo.MetaData{
"transaction_type": "savings",
},
},
ScheduledFor: &scheduledDate,
},
)
if err != nil {
return "", err
}
fmt.Println(
"Savings successfully scheduled:",
scheduledSavings.TransactionID,
)
return scheduledSavings.TransactionID, nil
}
scheduled_savings = blnk.transactions.create({
"precise_amount": 100000,
"precision": 100,
"reference": "SAV_0001",
"description": "Scheduled savings deposit",
"currency": "USD",
"source": "@WorldUSD",
"destination": savings_balance_id,
"scheduled_for": "2025-12-25T02:42:19Z",
"meta_data": {
"transaction_type": "savings",
},
})
print(
"Savings successfully scheduled:",
scheduled_savings.data["transaction_id"],
)
ApiResponse<JsonNode> scheduled_savings = blnk.transactions().create(
CreateTransactions.create()
.preciseAmount(100000)
.precision(100)
.reference("SAV_0001")
.description("Scheduled savings deposit")
.currency("USD")
.source("@WorldUSD")
.destination(savings_balance_id)
.scheduledFor("2025-12-25T02:42:19Z")
.metaData(Map.of("transaction_type", "savings"))
);
System.out.println(
"Savings successfully scheduled: "
+ scheduled_savings.data().get("transaction_id").asText()
);
SCHEDULED transaction is created waiting to be applied on the specified scheduled date.
Set up new scheduled transaction
To schedule the next savings deposit, verify thatscheduled_savings is still active on the customer’s balance and retrieve its frequency.
Blnk sends a webhook when a scheduled transaction gets applied.
# After a scheduled savings transaction is applied, update balance metadata:
curl -X POST "http://localhost:5001/balances/<savings-balance-id>/metadata" \
-H "X-blnk-key: <api-key>" \
-H "Content-Type: application/json" \
-d '{
"meta_data": {
"next_scheduled_savings": "2026-01-25T02:42:19Z"
}
}'
# Then create the next scheduled transaction:
curl -X POST "http://localhost:5001/transactions" \
-H "X-blnk-key: <api-key>" \
-H "Content-Type: application/json" \
-d '{
"precise_amount": 1000000,
"precision": 100,
"reference": "sch-savings-1709741652384",
"description": "Scheduled savings deposit",
"currency": "USD",
"source": "@WorldUSD",
"destination": "<savings-balance-id>",
"scheduled_for": "2026-01-25T02:42:19Z",
"meta_data": {
"transaction_type": "savings"
}
}'
import express from 'express';
import { RelatedTopics } from "/snippets/related-topics.jsx";
const app = express();
app.use(express.json());
app.post('/webhooks/transactions', async (req, res) => {
const { event, data } = req.body;
if (event === 'transaction.applied') {
const transaction = data;
if (transaction.meta_data?.transaction_type === 'savings') {
console.log(
'Scheduled savings completed:',
transaction.transaction_id,
);
const savingsBalanceId = transaction.destination;
const savingsBalance = await blnk.LedgerBalances.get(savingsBalanceId);
if (savingsBalance.meta_data?.schedule_savings === true) {
const frequency =
savingsBalance.meta_data.savings_frequency || 'monthly';
const amount = savingsBalance.meta_data.savings_amount;
const nextScheduledDate = calculateNextScheduledDate(frequency);
// 1. Update metadata with the next scheduled date
const baseUrl =
process.env.BLNK_BASE_URL ?? 'http://localhost:5001';
const apiKey = process.env.BLNK_API_KEY ?? '';
const metaRes = await fetch(
`${baseUrl}/${savingsBalanceId}/metadata`,
{
method: 'POST',
headers: {
'X-Blnk-Key': apiKey,
'Content-Type': 'application/json',
},
body: JSON.stringify({
meta_data: {
next_scheduled_savings: nextScheduledDate.toISOString(),
},
}),
},
);
if (!metaRes.ok) throw new Error(await metaRes.text());
// 2. Create a new scheduled transaction
const uniqueReference = `sch-savings-${Date.now()}`;
const scheduledSavings = await blnk.Transactions.create({
precise_amount: amount * 100,
precision: 100,
reference: uniqueReference,
description: 'Scheduled savings deposit',
currency: 'USD',
source: '@WorldUSD',
destination: savingsBalanceId,
scheduled_for: nextScheduledDate.toISOString(),
meta_data: {
transaction_type: 'savings',
},
});
console.log(
`Next scheduled deposit created for ${nextScheduledDate.toISOString()}:`,
scheduledSavings.data.transaction_id,
);
} else {
console.log(
`Scheduled savings is no longer active for balance ${savingsBalanceId}`,
);
}
}
}
res.status(200).send('Webhook received');
});
function calculateNextScheduledDate(frequency) {
const now = new Date();
const nextDate = new Date(now);
switch (frequency) {
case 'daily':
nextDate.setDate(now.getDate() + 1);
break;
case 'weekly':
nextDate.setDate(now.getDate() + 7);
break;
case 'biweekly':
nextDate.setDate(now.getDate() + 14);
break;
case 'monthly':
nextDate.setMonth(now.getMonth() + 1);
break;
default:
nextDate.setMonth(now.getMonth() + 1);
}
return nextDate;
}
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Webhook server listening on port ${PORT}`);
});
func handleTransactionWebhook(w http.ResponseWriter, r *http.Request) {
var payload struct {
Event string `json:"event"`
Data map[string]interface{} `json:"data"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
http.Error(w, "invalid payload", http.StatusBadRequest)
return
}
if payload.Event != "transaction.applied" {
w.WriteHeader(http.StatusOK)
return
}
meta, _ := payload.Data["meta_data"].(map[string]interface{})
if meta["transaction_type"] != "savings" {
w.WriteHeader(http.StatusOK)
return
}
client := getClient()
savingsBalanceID, _ := payload.Data["destination"].(string)
savingsBalance, _, err := client.LedgerBalance.Get(savingsBalanceID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
balanceMeta := savingsBalance.MetaData
if scheduleSavings, _ := balanceMeta["schedule_savings"].(bool); !scheduleSavings {
w.WriteHeader(http.StatusOK)
return
}
frequency, _ := balanceMeta["savings_frequency"].(string)
if frequency == "" {
frequency = "monthly"
}
amount, _ := balanceMeta["savings_amount"].(float64)
nextScheduledDate := calculateNextScheduledDate(frequency)
// 1. Update metadata with the next scheduled date
_, _, err = client.Metadata.UpdateMetadata(
savingsBalanceID,
blnkgo.UpdateMetaDataRequest{
MetaData: blnkgo.MetaData{
"next_scheduled_savings": nextScheduledDate.Format(time.RFC3339),
},
},
)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// 2. Create a new scheduled transaction
uniqueReference := fmt.Sprintf("sch-savings-%d", time.Now().Unix())
scheduledSavings, _, err := client.Transaction.Create(
blnkgo.CreateTransactionRequest{
ParentTransaction: blnkgo.ParentTransaction{
PreciseAmount: amount * 100,
Precision: 100,
Reference: uniqueReference,
Description: "Scheduled savings deposit",
Currency: "USD",
Source: "@WorldUSD",
Destination: savingsBalanceID,
MetaData: blnkgo.MetaData{
"transaction_type": "savings",
},
},
ScheduledFor: &nextScheduledDate,
},
)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Printf(
"Next scheduled deposit created for %s: %s\n",
nextScheduledDate.Format(time.RFC3339),
scheduledSavings.TransactionID,
)
w.WriteHeader(http.StatusOK)
}
from datetime import datetime, timedelta
from flask import Flask, request
app = Flask(__name__)
@app.post("/webhooks/transactions")
def handle_transaction_webhook():
payload = request.get_json()
event = payload.get("event")
data = payload.get("data", {})
if event != "transaction.applied":
return "Webhook received", 200
if (data.get("meta_data") or {}).get("transaction_type") != "savings":
return "Webhook received", 200
print("Scheduled savings completed:", data.get("transaction_id"))
savings_balance_id = data["destination"]
savings_balance = blnk.ledger_balances.get(savings_balance_id)
balance_meta = savings_balance.data.get("meta_data") or {}
if balance_meta.get("schedule_savings") is not True:
print(
f"Scheduled savings is no longer active for balance {savings_balance_id}"
)
return "Webhook received", 200
frequency = balance_meta.get("savings_frequency") or "monthly"
amount = balance_meta.get("savings_amount")
next_scheduled_date = calculate_next_scheduled_date(frequency)
# 1. Update metadata with the next scheduled date
blnk.metadata.update(savings_balance_id, {
"meta_data": {
"next_scheduled_savings": next_scheduled_date.isoformat(),
},
})
# 2. Create a new scheduled transaction
unique_reference = f"sch-savings-{int(datetime.now().timestamp())}"
scheduled_savings = blnk.transactions.create({
"precise_amount": amount * 100,
"precision": 100,
"reference": unique_reference,
"description": "Scheduled savings deposit",
"currency": "USD",
"source": "@WorldUSD",
"destination": savings_balance_id,
"scheduled_for": next_scheduled_date.isoformat(),
"meta_data": {
"transaction_type": "savings",
},
})
print(
f"Next scheduled deposit created for {next_scheduled_date.isoformat()}:",
scheduled_savings.data["transaction_id"],
)
return "Webhook received", 200
def calculate_next_scheduled_date(frequency):
now = datetime.now()
if frequency == "daily":
return now + timedelta(days=1)
if frequency == "weekly":
return now + timedelta(days=7)
if frequency == "biweekly":
return now + timedelta(days=14)
# monthly (default)
return now + timedelta(days=30)
if __name__ == "__main__":
app.run(port=3000)
// Spring Boot-style webhook handler sketch
@PostMapping("/webhooks/transactions")
public ResponseEntity<String> handleTransactionWebhook(@RequestBody JsonNode payload) {
String event = payload.path("event").asText();
JsonNode data = payload.path("data");
if (!"transaction.applied".equals(event)) {
return ResponseEntity.ok("Webhook received");
}
if (!"savings".equals(data.path("meta_data").path("transaction_type").asText())) {
return ResponseEntity.ok("Webhook received");
}
String savingsBalanceId = data.path("destination").asText();
ApiResponse<JsonNode> savingsBalance = blnk.ledgerBalances().get(savingsBalanceId);
JsonNode balanceMeta = savingsBalance.data().path("meta_data");
if (!balanceMeta.path("schedule_savings").asBoolean(false)) {
return ResponseEntity.ok("Webhook received");
}
String frequency = balanceMeta.path("savings_frequency").asText("monthly");
long amount = balanceMeta.path("savings_amount").asLong();
Instant nextScheduledDate = calculateNextScheduledDate(frequency);
// 1. Update metadata with the next scheduled date
blnk.metadata().update(
savingsBalanceId,
UpdateMetadataData.create()
.metaData(Map.of(
"next_scheduled_savings",
nextScheduledDate.toString()
))
);
// 2. Create a new scheduled transaction
String uniqueReference = "sch-savings-" + Instant.now().getEpochSecond();
blnk.transactions().create(
CreateTransactions.create()
.preciseAmount(amount * 100)
.precision(100)
.reference(uniqueReference)
.description("Scheduled savings deposit")
.currency("USD")
.source("@WorldUSD")
.destination(savingsBalanceId)
.scheduledFor(nextScheduledDate.toString())
.metaData(Map.of("transaction_type", "savings"))
);
return ResponseEntity.ok("Webhook received");
}
Change savings frequency
To change the savings frequency of a balance, use the Update Metadata endpoint: Update the savings frequency on the customer balance:curl -X POST "http://localhost:5001/balances/<balance-id>/metadata" \
-H "X-blnk-key: <api-key>" \
-H "Content-Type: application/json" \
-d '{
"meta_data": {
"savings_frequency": "daily"
}
}'
async function updateSavingsFrequency(balanceId, newFrequency) {
try {
const baseUrl =
process.env.BLNK_BASE_URL ?? 'http://localhost:5001';
const apiKey = process.env.BLNK_API_KEY ?? '';
const res = await fetch(`${baseUrl}/${balanceId}/metadata`, {
method: 'POST',
headers: {
'X-Blnk-Key': apiKey,
'Content-Type': 'application/json',
},
body: JSON.stringify({
meta_data: { savings_frequency: newFrequency },
}),
});
if (!res.ok) throw new Error(await res.text());
const data = await res.json();
console.log(
`Successfully updated savings frequency to ${newFrequency} for balance ${balanceId}`,
);
return data;
} catch (error) {
console.error(
`Failed to update savings frequency for balance ${balanceId}:`,
error,
);
throw error;
}
}
func updateSavingsFrequency(balanceID, newFrequency string) error {
client := getClient()
_, _, err := client.Metadata.UpdateMetadata(
balanceID,
blnkgo.UpdateMetaDataRequest{
MetaData: blnkgo.MetaData{
"savings_frequency": newFrequency,
},
},
)
if err != nil {
return err
}
fmt.Printf(
"Successfully updated savings frequency to %s for balance %s\n",
newFrequency,
balanceID,
)
return nil
}
blnk.metadata.update(balance_id, {
"meta_data": {
"savings_frequency": new_frequency,
},
})
print(
f"Successfully updated savings frequency to {new_frequency} for balance {balance_id}"
)
ApiResponse<JsonNode> response = blnk.metadata().update(
balance_id,
UpdateMetadataData.create()
.metaData(Map.of("savings_frequency", new_frequency))
);
Conclusion
In this tutorial, we’ve built a scheduled savings workflow for a wallet app using Blnk. Here’s what we covered:- Setting up savings ledgers and customer savings accounts
- Adding scheduled deposits
- Automatically rescheduling deposits after they’re completed
- Handling deposit frequencies and customer preferences
- Using webhooks to detect completed transactions and schedule new deposits.