<!-- Source: https://blnkfinance.com/blog/how-to-implement-recurring-payments-with-blnk -->

[Blog](https://blnkfinance.com/blog) [Build](https://blnkfinance.com/blog/build)

Apr 2, 2026 4 min read

# How to Build Recurring Payments and Savings Goals with Blnk Ledger

-   Emmanuella Etop-Essien Developer Relations

![How to build recurring payments and savings goals with Blnk Ledger](https://cdn.sanity.io/images/06pses5q/production/1f380bcda8fadbce093afa5e7cab80194f460f51-5760x3240.webp?w=1600&fit=max&auto=format)

On this page

1.  [Mental model](#mental-model)
2.  [Step 1: Setting up your ledger design](#step-1-setting-up-your-ledger-design)
3.  [Step 2: Schedule automated payments](#step-2-schedule-automated-payments)
4.  [Step 3: Tracking goal progress](#step-3-tracking-goal-progress)
5.  [Wrapping up](#wrapping-up)
6.  [What else can you build?](#what-else-can-you-build)

Mia opens Kite, a personal finance app, sets a savings goal, and picks her terms: $200 a month for 3 months. She confirms it and moves on. From that point, she expects the deposits to happen on their own.

For you as the developer, a fixed savings goal gives you everything upfront: the amount, the deposit count, and the exact dates. In this guide, you'll set up Mia's savings balance, schedule all three deposits in a single bulk request, and query her progress, all with Blnk.

## \# Mental model

Before you touch any code, define how money should move when Mia's goal is active. For Kite, there's one movement:

-   **Deposit:** `@BankDeposits` → `bln_mia_savings`. Each [scheduled](https://docs.blnkfinance.com/transactions/scheduling) deposit moves $200 from the bank deposit source into Mia's savings balance on its due date.

`@BankDeposits` is a [system balance](https://docs.blnkfinance.com/balances/internal-balances) that represents external inflows. Blnk creates it automatically the first time you reference it. In production, you define this name to map to your actual funding source.

![](https://cdn.sanity.io/images/06pses5q/production/0c1d5b4593a68939f97e664ab1f46fb63f5d7220-1539x847.png?w=1539&fit=max&auto=format)

The money movement map shows $200 flowing from @BankDeposits into Mia's savings balance on the 1st of each month across three scheduled deposits. See it with our Money Movement Map tool.

## \# Step 1: Setting up your ledger design

Make sure you have a [deployed Blnk instance](https://docs.blnkfinance.com/cloud/start/guide) before you start.

For Mia's savings goal, you need three things in place before you can schedule any transactions.

To implement this with Blnk:

1.  **Create a ledger called Savings Ledger** to group all savings balances. [Read docs.](https://docs.blnkfinance.com/ledgers/introduction)
2.  **Create an [identity for Mia](https://blnkfinance.com/glossary/identity)** so you can attach her savings balance to her profile. [Read docs.](https://docs.blnkfinance.com/ledgers/introduction)
3.  **Create a savings balance for Mia** under the Savings Ledger, linked to her identity. [Read docs.](https://docs.blnkfinance.com/ledgers/introduction)

Save the `ledger_id`, `identity_id`, and `balance_id` Blnk returns. You'll reference the `balance_id` as `destination` in every scheduled deposit, and the ledger and identity IDs when setting up savings for other customers.

## \# Step 2: Schedule automated payments

When Mia activates her goal, your app has three inputs:

-   The amount ($200 per deposit)
-   The frequency (monthly)
-   The count (the number of deposits, 3 in this case)

Use those to calculate the exact date for each deposit, then send all three transactions in one bulk request. Each transaction has a [`scheduled_for`](https://blnkfinance.com/glossary/scheduled-transaction) value set to the 1st of its respective month.

<!-- TypeScript -->
```
const response = await blnk.Transactions.createBulk({
  transactions: [
    {
      amount: 20000,
      precision: 100,
      currency: 'USD',
      reference: 'save_mia_2026-03',
      source: '@BankDeposits',
      destination: 'bln_mia_savings',
      description: 'Monthly savings deposit - March 2026',
      allow_overdraft: true,
      scheduled_for: '2026-03-01T08:00:00+00:00',
      meta_data: {
        goal_id: 'goal_mia_001',
        period: '2026-03',
      },
    },
    {
      amount: 20000,
      precision: 100,
      currency: 'USD',
      reference: 'save_mia_2026-04',
      source: '@BankDeposits',
      destination: 'bln_mia_savings',
      description: 'Monthly savings deposit - April 2026',
      allow_overdraft: true,
      scheduled_for: '2026-04-01T08:00:00+00:00',
      meta_data: {
        goal_id: 'goal_mia_001',
        period: '2026-04',
      },
    },
    {
      amount: 20000,
      precision: 100,
      currency: 'USD',
      reference: 'save_mia_2026-05',
      source: '@BankDeposits',
      destination: 'bln_mia_savings',
      description: 'Monthly savings deposit - May 2026',
      allow_overdraft: true,
      scheduled_for: '2026-05-01T08:00:00+00:00',
      meta_data: {
        goal_id: 'goal_mia_001',
        period: '2026-05',
      },
    },
  ],
});
```

<!-- Go -->
```
result, resp, err := client.Transaction.CreateBulk(blnkgo.CreateBulkTransactionRequest{

  Transactions: []blnkgo.CreateTransactionRequest{
    blnkgo.CreateTransactionRequest{
      ParentTransaction: blnkgo.ParentTransaction{
        Amount: 20000,
        Precision: 100,
        Currency: "USD",
        Reference: "save_mia_2026-03",
        Source: "@BankDeposits",
        Destination: "bln_mia_savings",
        Description: "Monthly savings deposit - March 2026",
        ScheduledFor: "2026-03-01T08:00:00+00:00",
        MetaData: blnkgo.MetaData{
          "goal_id": "goal_mia_001",
          "period": "2026-03",
        },
      },
      AllowOverdraft: true,
    },
    blnkgo.CreateTransactionRequest{
      ParentTransaction: blnkgo.ParentTransaction{
        Amount: 20000,
        Precision: 100,
        Currency: "USD",
        Reference: "save_mia_2026-04",
        Source: "@BankDeposits",
        Destination: "bln_mia_savings",
        Description: "Monthly savings deposit - April 2026",
        ScheduledFor: "2026-04-01T08:00:00+00:00",
        MetaData: blnkgo.MetaData{
          "goal_id": "goal_mia_001",
          "period": "2026-04",
        },
      },
      AllowOverdraft: true,
    },
    blnkgo.CreateTransactionRequest{
      ParentTransaction: blnkgo.ParentTransaction{
        Amount: 20000,
        Precision: 100,
        Currency: "USD",
        Reference: "save_mia_2026-05",
        Source: "@BankDeposits",
        Destination: "bln_mia_savings",
        Description: "Monthly savings deposit - May 2026",
        ScheduledFor: "2026-05-01T08:00:00+00:00",
        MetaData: blnkgo.MetaData{
          "goal_id": "goal_mia_001",
          "period": "2026-05",
        },
      },
      AllowOverdraft: true,
    },
  },
})
```

<!-- Python -->
```
response = blnk.transactions.create_bulk({
  "transactions": [
    {
      "amount": 20000,
      "precision": 100,
      "currency": "USD",
      "reference": "save_mia_2026-03",
      "source": "@BankDeposits",
      "destination": "bln_mia_savings",
      "description": "Monthly savings deposit - March 2026",
      "allow_overdraft": True,
      "scheduled_for": "2026-03-01T08:00:00+00:00",
      "meta_data": {
        "goal_id": "goal_mia_001",
        "period": "2026-03",
      },
    },
    {
      "amount": 20000,
      "precision": 100,
      "currency": "USD",
      "reference": "save_mia_2026-04",
      "source": "@BankDeposits",
      "destination": "bln_mia_savings",
      "description": "Monthly savings deposit - April 2026",
      "allow_overdraft": True,
      "scheduled_for": "2026-04-01T08:00:00+00:00",
      "meta_data": {
        "goal_id": "goal_mia_001",
        "period": "2026-04",
      },
    },
    {
      "amount": 20000,
      "precision": 100,
      "currency": "USD",
      "reference": "save_mia_2026-05",
      "source": "@BankDeposits",
      "destination": "bln_mia_savings",
      "description": "Monthly savings deposit - May 2026",
      "allow_overdraft": True,
      "scheduled_for": "2026-05-01T08:00:00+00:00",
      "meta_data": {
        "goal_id": "goal_mia_001",
        "period": "2026-05",
      },
    },
  ],
})
```

<!-- Java -->
```
ApiResponse<JsonNode> response = blnk.transactions().createBulk(
  CreateBulkTransactions.create()
    .transactions(List.of(
    Map.ofEntries(
      Map.entry("amount", 20000),
      Map.entry("precision", 100),
      Map.entry("currency", "USD"),
      Map.entry("reference", "save_mia_2026-03"),
      Map.entry("source", "@BankDeposits"),
      Map.entry("destination", "bln_mia_savings"),
      Map.entry("description", "Monthly savings deposit - March 2026"),
      Map.entry("allow_overdraft", true),
      Map.entry("scheduled_for", "2026-03-01T08:00:00+00:00"),
      Map.entry("meta_data", Map.of("goal_id", "goal_mia_001", "period", "2026-03"))),
      Map.ofEntries(
        Map.entry("amount", 20000),
        Map.entry("precision", 100),
        Map.entry("currency", "USD"),
        Map.entry("reference", "save_mia_2026-04"),
        Map.entry("source", "@BankDeposits"),
        Map.entry("destination", "bln_mia_savings"),
        Map.entry("description", "Monthly savings deposit - April 2026"),
        Map.entry("allow_overdraft", true),
        Map.entry("scheduled_for", "2026-04-01T08:00:00+00:00"),
        Map.entry("meta_data", Map.of("goal_id", "goal_mia_001", "period", "2026-04"))),
        Map.ofEntries(
          Map.entry("amount", 20000),
          Map.entry("precision", 100),
          Map.entry("currency", "USD"),
          Map.entry("reference", "save_mia_2026-05"),
          Map.entry("source", "@BankDeposits"),
          Map.entry("destination", "bln_mia_savings"),
          Map.entry("description", "Monthly savings deposit - May 2026"),
          Map.entry("allow_overdraft", true),
          Map.entry("scheduled_for", "2026-05-01T08:00:00+00:00"),
          Map.entry("meta_data", Map.of("goal_id", "goal_mia_001", "period", "2026-05"))))));
```

<!-- 200 OK -->
```
{
  "batch_id": "bulk_mia_goal_001",
  "status": "queued",
  "transaction_count": 3
}
```

**Here's what happens:**

-   Blnk records all the transactions as `SCHEDULED` . They get processed as soon as the time is reached, and her balance is topped up.
-   Each `reference` is unique per period; it acts as an idempotency key. If your app sends the same request twice, Blnk discards the duplicate.
-   `allow_overdraft: true` on `@BankDeposits` lets the system balance go negative. This is the standard pattern for representing external inflows.
-   Add a shared ID like `goal_id` to group together all transactions for this plan.

![](https://cdn.sanity.io/images/06pses5q/production/b4a02ca1f0d1e7a9fe0abf28a7428e1d303774fe-1362x746.png?w=1362&fit=max&auto=format)

Transactions table showing all three scheduled deposits for Mia's savings goal, each with status QUEUED and their respective scheduled dates.

## \# Step 3: Tracking goal progress

To show Mia how much she's saved so far, read her savings balance:

<!-- TypeScript -->
```
const response = await blnk.LedgerBalances.get(
  'bln_mia_savings',
);
```

<!-- Go -->
```
balance, resp, err := client.LedgerBalance.Get(
  "bln_mia_savings",
)
```

<!-- Python -->
```
response = blnk.ledger_balances.get(
  "bln_mia_savings",
)
```

<!-- Java -->
```
ApiResponse<JsonNode> response = blnk.ledgerBalances().get(
  "bln_mia_savings");
```

<!-- 200 OK -->
```
{
  "balance_id": "bln_mia_savings",
  "balance": 40000,
  "credit_balance": 40000,
  "debit_balance": 0,
  "currency": "USD",
  "precision": 100,
  "ledger_id": "ldg_savings_001",
  "identity_id": "idt_mia_001",
  "created_at": "2026-03-01T08:00:00Z"
}
```

**Here's what happens:**

-   `balance` is returned in the smallest unit. Divide by `precision` (100) to get the display amount: 40000 / 100 = $400 saved so far (March and April deposits applied).
-   `credit_balance` reflects total money received into the balance; `debit_balance` reflects total money sent out. For a savings balance that only receives deposits, `debit_balance` stays at 0.

To see which deposits have run and which are still scheduled, query by `goal_id`:

<!-- TypeScript -->
```
const response = await blnk.Search.filter(
  {
    filters: [
      {
        field: 'meta_data.goal_id',
        operator: 'eq',
        value: 'goal_mia_001',
      },
    ],
  },
  'transactions',
);
```

<!-- Go -->
```
result, resp, err := client.Transaction.Filter(
  blnkgo.FilterParams{
    Filters: []blnkgo.Filter{
      {
        Field: "meta_data.goal_id",
        Operator: blnkgo.OpEqual,
        Value: "goal_mia_001",
      }
    },
  },
)
```

<!-- Python -->
```
response = blnk.search.filter(
  {
    "filters": [
      {
        "field": "meta_data.goal_id",
        "operator": "eq",
        "value": "goal_mia_001",
      },
    ],
  },
  "transactions",
)
```

<!-- Java -->
```
ApiResponse<JsonNode> response = blnk.search().filter(
  FilterParams.create()
    .filters(List.of(
    Map.of("field", "meta_data.goal_id", "operator", "eq", "value", "goal_mia_001"))),
    "transactions");
```

<!-- 200 OK -->
```
[
  {
    "transaction_id": "txn_save_mia_001",
    "reference": "save_mia_2026-03",
    "amount": 20000,
    "currency": "USD",
    "status": "APPLIED",
    "scheduled_for": "2026-03-01T08:00:00+00:00",
    "meta_data": {
      "goal_id": "goal_mia_001",
      "period": "2026-03"
    }
  },
  {
    "transaction_id": "txn_save_mia_002",
    "reference": "save_mia_2026-04",
    "amount": 20000,
    "currency": "USD",
    "status": "APPLIED",
    "scheduled_for": "2026-04-01T08:00:00+00:00",
    "meta_data": {
      "goal_id": "goal_mia_001",
      "period": "2026-04"
    }
  },
  {
    "transaction_id": "txn_save_mia_003",
    "reference": "save_mia_2026-05",
    "amount": 20000,
    "currency": "USD",
    "status": "QUEUED",
    "scheduled_for": "2026-05-01T08:00:00+00:00",
    "meta_data": {
      "goal_id": "goal_mia_001",
      "period": "2026-05"
    }
  }
]
```

**Here's what happens:**

-   Each transaction has a `status`: `APPLIED` for deposits that have run, `QUEUED` for ones still scheduled.

## \# Wrapping up

You now have a savings goal that runs completely on schedule. Mia confirmed her goal once; Blnk handles the rest: applying each deposit on the right date, updating her balance, and keeping a full audit trail.

This same pattern works for any recurring payment with a **known duration**: installment plans, fixed-term subscriptions, [scheduled loan repayments](https://blnkfinance.com/blog/how-to-build-loan-disbursement-interest-and-repayments-with-blnk), or any flow where you know the amount and the count upfront.

To see this flow end-to-end, run the [savings goal demo](https://github.com/blnkfinance/blnk-demo/tree/recurring-payments) in the blnk-demo repo.

## \# **What else can you build?**

With this foundation you can:

-   **Pause a goal:** identify which scheduled transactions haven't run yet and set them to inflight. If the customer cancels, void them.
-   **Adjust the amount:** cancel remaining scheduled transactions and recreate them with the updated amount.
-   **Extend the goal:** add more scheduled transactions to cover the new periods.
-   **Notify on each deposit:** listen for [`transaction.applied`](https://docs.blnkfinance.com/webhooks/overview) webhooks to send Mia a confirmation when each month's deposit lands.
-   If you're processing payments to multiple recipients at once, see how to build a [bulk payroll system](https://blnkfinance.com/blog/how-to-build-a-bulk-payroll-system-using-the-blnk-ledger) with the same API.

For more on Blnk, check the [**Blnk docs**](https://docs.blnkfinance.com/home/install) or join the community on [**Discord**](https://discord.gg/7WNv94zPpx) to ask questions and see what others are building.

## Related posts

-   [
    
    Sep 11, 2026Build
    
    ### Building a correct inventory system on a double-entry ledger
    
    Praise Philemon
    
    ](https://blnkfinance.com/blog/building-an-inventory-system-with-a-ledger)
-   [
    
    Sep 7, 2026Build
    
    ### Why one payment does not always equal one transaction.
    
    Emmanuella Etop-Essien
    
    ](https://blnkfinance.com/blog/why-one-payment-does-not-always-equal-one-transaction)
-   [
    
    Sep 3, 2026Build
    
    ### Building a BaaS Ledger, Part 2: Transaction Workflows
    
    Praise Philemon
    
    ](https://blnkfinance.com/blog/how-to-build-a-baas-ledger-part-2)
