<!-- Source: https://blnkfinance.com/blog/how-to-generate-customer-statements-from-your-ledger-with-blnk-postgres -->

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

6 min read

# How to Generate Customer Account Statements from Your Fintech Ledger with Blnk

Generate customer statements from Blnk and Postgres. Opening and closing balances, the transactions in between, and how to query them.

![How to generate customer account statements from your fintech ledger with Blnk](https://cdn.sanity.io/images/06pses5q/production/0e30c9bc1cc4529c63d6d5c09f6f8f59ae0f3daf-1920x1080.webp?w=1600&fit=max&auto=format)

It’s the end of the quarter, and one of your customers wants a clear view of their transaction activity. They ask for a statement covering the period. What do you do?

Customer statements are simply a structured view of a balance’s activity over a specific period. They are a core part of financial reporting in any fintech product built on a double-entry bookkeeping system. They usually include an [opening balance](https://blnkfinance.com/glossary/historical-balance), in-period debits and credits, closing balance, and a list of transactions within the period.

In practice, customer statements can be more than bank account statements. Common examples include card statements, loan statements, invoice history, savings history, and stock activity reports. You can also use this same approach to generate payroll statements for [bulk payment runs](https://blnkfinance.com/blog/how-to-build-a-bulk-payroll-system-using-the-blnk-ledger).

How you generate a statement depends on how your ledger stores its balances and transactions. Blnk keeps both as first-class citizens, which makes statement generation super easy to implement.

If you’d like to follow along hands-on, you can try this end to end using our [**example demo here.**](https://github.com/blnkfinance/blnk-demo/tree/main/customer-statements)

Let’s get started.

## Getting started with account statement generation

To generate a statement, you need the following inputs:

-   `:balance_id`: The customer’s account/wallet in your ledger.
-   `:currency`: The currency of the balance.
-   `:period_start`: Start date of the statement period (timestamp).
-   `:period_end`: End date of the statement period (timestamp).

Blnk's **Balances** and **Transactions** modules give you everything you need to build your customer statements from your fintech ledger.

-   **Balance:** Acts a store of value. Each balance tracks core attributes like `balance`, `credit_balance`, `debit_balance`. As a rule, balances are immutable and change only via transactions. [**Read docs.**](https://docs.blnkfinance.com/balances/introduction)
-   **Transaction:** Represents events that happen on your balance via double-entry, e.g. deposits, withdrawals, currency conversion, etc.

## Prerequisites

Before you start building, make sure you have the following ready:

-   Your Core instance is up and running.
-   If it’s a new instance, ensure you have some data in it. Here’s a [**demo population script**](https://github.com/blnkfinance/blnk-demo/tree/main/populate) to get you started.
-   Your DB credentials to connect and make queries from your code.

## Step 1: Get transactions for financial reporting

First, we'll use Blnk Search API to retrieve all `APPLIED` transactions where the customer’s balance ID appears as either `source` or `destination`.

Blnk Search uses UNIX timestamps, so make sure you convert your start and end dates. Blnk records timestamps in UTC by default.

<!-- TypeScript -->
```
const response = await blnk.Search.search(
  {
    q: 'bln_customer-id',
    query_by: 'source,destination',
    filter_by: 'effective_date:[<utc-period-start>..<utc-period-end>] && status:=APPLIED',
    sort_by: 'effective_date:asc',
    page: 1,
    per_page: 250,
  },
  'transactions',
);
```

<!-- Go -->
```
searchResult, resp, err := client.Search.SearchDocument(
  blnkgo.SearchParams{
    Q: "bln_customer-id",
    QueryBy: "source,destination",
    FilterBy: "effective_date:[<utc-period-start>..<utc-period-end>] && status:=APPLIED",
    SortBy: "effective_date:asc",
    Page: 1,
    PerPage: 250,
  },
  blnkgo.Transactions,
)
```

<!-- Python -->
```
response = blnk.search.search(
  {
    "q": "bln_customer-id",
    "query_by": "source,destination",
    "filter_by": "effective_date:[<utc-period-start>..<utc-period-end>] && status:=APPLIED",
    "sort_by": "effective_date:asc",
    "page": 1,
    "per_page": 250,
  },
  "transactions",
)
```

<!-- Java -->
```
ApiResponse<JsonNode> response = blnk.search().search(
  SearchParams.create()
    .q("bln_customer-id")
    .queryBy("source,destination")
    .filterBy("effective_date:[<utc-period-start>..<utc-period-end>] && status:=APPLIED")
    .sortBy("effective_date:asc")
    .page(1)
    .perPage(250),
  "transactions");
```

-   `per_page` has a maximum of 250. Paginate until you have the full set.
-   Conventionally, account statements only show transactions that have been applied to your balance. That’s why we filtered for only the `APPLIED` status.

<!-- 200 OK -->
```
{
  "facet_counts": [],
  "found": 150,
  "hits": [
    // List of all transactions
  ],
  "page": 1,
  "per_page": 250,
}
```

-   `found` gives you the total number of transactions applied within that specified period.

## Step 2: Credit & debit totals, opening & closing balances

Next, we want to compute our statement summary info:

-   **Total credits:** Total sum received by the balance.
-   **Total debits:** Total sum sent by the balance.
-   **Opening balance:** How much was in the balance at the start of the statement period.
-   **Closing balance:** How much was in the balance at the end of the statement period.

To compute this, we’ll use Blnk’s [Historical Balances endpoint](https://docs.blnkfinance.com/balances/historical-balances). This endpoint lets us resolve the exact state of a balance at a specific point in time.

We’ll call it twice:

1.  Once at the start of the statement period.
2.  Once at the end of the statement period.

### Fetch the balance at the start and end of the period

Blnk expects an ISO timestamp in the request.

<!-- TypeScript -->
```
const response = await blnk.LedgerBalances.getAt(
  '{balance_id}',
  {
    timestamp: '{iso_timestamp}',
  },
);
```

<!-- Go -->
```
balance, resp, err := client.LedgerBalance.GetAt(
  "{balance_id}",
  blnkgo.GetBalanceAtRequest{
    Timestamp: "{iso_timestamp}",
  },
)
```

<!-- Python -->
```
response = blnk.ledger_balances.get_at(
  "{balance_id}",
  {
    "timestamp": "{iso_timestamp}",
  },
)
```

<!-- Java -->
```
ApiResponse<JsonNode> response = blnk.ledgerBalances().getAt(
  "{balance_id}",
  GetBalanceAtRequest.create()
    .timestamp("{iso_timestamp}"));
```

Each call returns a snapshot of the balance at that moment.

<!-- 200 OK -->
```
{
  "balance": {
    "balance": 9620000,
    "balance_id": "bin_be16c4a1-b5a6-4b64-a733-de2f6b24813d",
    "credit_balance": 9620000,
    "currency": "NGN",
    "debit_balance": 0,
    "ledger_id": "ldg_383739-8383749-38380-83373630-373363d"
  },
  "timestamp": "2025-02-24T08:55:26Z"
}
```

### Identify opening and closing balances

-   **Opening balance:** This is `balance.balance` from the response when you queried the period start.
-   **Closing balance:** This is `balance.balance` from the response when you queried the period end.

No calculations needed here. You’re simply reading the resolved balance values at two points in time.

### Compute total credits for the period

`credit_balance` is cumulative. So to get credits within the statement period, subtract the credit\_balance at the start period from its value at the end period.

```
total_credits =
period_end.balance.credit_balance
- period_start.balance.credit_balance
```

### Compute total debits for the period

The same logic as computing total credits applies to debits as well.

```
total_credits =
period_end.balance.credit_balance
- period_start.balance.credit_balance
```

### Mental model

This is how to think about this.

-   Historical Balances give you snapshots.
-   `credit_balance` and `debit_balance` always move forward.
-   Period totals are just differences between two snapshots.

This is why you don’t need to scan transactions again from your DB to compute totals. Blnk has already done that work for you.

## Step 4: Format for user-friendly account statements

### Convert amounts and balances to user-friendly units.

Blnk returns amounts in **precise units** (usually the smallest unit, like cents). Before rendering CSV/PDF, convert them to the display unit using the currency’s [precision](https://blnkfinance.com/glossary/precision).

```
function toDisplayAmount(amount_in_minor_units, precision):
  return amount_in_minor_units / precision
```

Apply this to `opening_balance`, `closing_balance`, `total_credits`, and `total_debits`.

### Determine direction (DR vs CR)

**Rule of thumb:** If `balance_id` is the source, it is DR. If `balance_id` is the destination, it is CR.

```
function getDirection(tx, balance_id):
  if tx.source == balance_id:
    return "DR"
    if tx.destination == balance_id:
      return "CR"
```

### Convert timestamps to the user’s local timezone.

Convert `effective_date` to display in the user’s timezone.

```
function toUserTimezone(utc_timestamp, user_timezone):
  dt = parseISO(utc_timestamp)          // parse as UTC
  return convertTimezone(dt, user_timezone)
```

Also, decide your display format (important for statements):

```
function formatStatementTime(dt):
  return format(dt, "YYYY-MM-DD HH:mm:ss")
```

### Determine counterparty and fetch their details

Rule:

-   If `balance_id` is source, counterparty is destination.
-   If `balance_id` is destination, counterparty is source.

Then resolve counterparty details (identity\_id, metadata) by calling your balance or search endpoint.

```
function getCounterpartyId(tx, balance_id):
  if tx.source == balance_id:
    return tx.destination
    if tx.destination == balance_id:
      return tx.source
      return null

      function getCounterpartyName(counterparty_balance_id):
        api.get("/balances/" + counterparty_balance_id)
        return response.meta_data.name
```

## Step 5: Put it together (per transaction row)

At this point, you already have everything you need to finalize your statement. The final step is formatting this data so that it can be cleanly presented to users in CSV or PDF.

```
for tx in transactions:
  direction = getDirection(tx, balance_id)
  counterparty_id = getCounterpartyId(tx, balance_id)
  counterparty = getCounterpartyName(counterparty_id)
  row = {
    timestamp: formatStatementTime(toUserTimezone(tx.effective_date, user_timezone)),
    reference: tx.reference,
    description: tx.description,
    direction: direction,
    counterparty: counterparty.name,              // or id if name not available
    amount: toDisplayAmount(tx.amount, tx.currency),
    currency: tx.currency
  }
  statement_rows.append(row)
```

Your statement should now look like this, and you can use it to prepare your CSV/JSON:

<!-- 200 OK -->
```
{
  "statement": {
    "balance_id": "bln_...",
    "currency": "USD",
    "account_name": "Emily Whiskerson",
    "period": {
      "start": "2026-01-01T00:00:00Z"
      "end": "2026-01-31T23:59:59Z"
    },
    "opening_balance": "500.00",
    "closing_balance": "845.00",
    "totals": {
      "credits": "600.00",
      "debits": "255.00",
      "transaction_count": 10
    },
    "transactions": [
      {
        "timestamp": "2026-01-01T8:23:14Z",
        "reference": "payment_001",
        "description": "Card top-up",
        "direction": "CR",
        "amount": 10.00
      },
        ...transactions
    ]
  }
}
```

## Give it a try

If you haven’t yet, install or deploy Blnk, and run the customer statements demo on [github.com/blnkfinance/blnk-demo](https://github.com/blnkfinance/blnk-demo).

On this page

1.  [Getting started with account statement generation](#getting-started-with-account-statement-generation)
2.  [Prerequisites](#prerequisites)
3.  [Step 1: Get transactions for financial reporting](#step-1-get-transactions-for-financial-reporting)
4.  [Step 2: Credit & debit totals, opening & closing balances](#step-2-credit-debit-totals-opening-closing-balances)
5.  [Fetch the balance at the start and end of the period](#fetch-the-balance-at-the-start-and-end-of-the-period)
6.  [Identify opening and closing balances](#identify-opening-and-closing-balances)
7.  [Compute total credits for the period](#compute-total-credits-for-the-period)
8.  [Compute total debits for the period](#compute-total-debits-for-the-period)
9.  [Mental model](#mental-model)
10.  [Step 4: Format for user-friendly account statements](#step-4-format-for-user-friendly-account-statements)
11.  [Convert amounts and balances to user-friendly units.](#convert-amounts-and-balances-to-user-friendly-units)
12.  [Determine direction (DR vs CR)](#determine-direction-dr-vs-cr)
13.  [Convert timestamps to the user’s local timezone.](#convert-timestamps-to-the-user-s-local-timezone)
14.  [Determine counterparty and fetch their details](#determine-counterparty-and-fetch-their-details)
15.  [Step 5: Put it together (per transaction row)](#step-5-put-it-together-per-transaction-row)
16.  [Give it a try](#give-it-a-try)

Author

-    ![](https://cdn.sanity.io/images/06pses5q/production/140e675048b3b044758196586e139361bbc94f31-500x500.jpg?w=80&h=80&fit=crop&auto=format)Praise Philemon Co-founder & Head of Revenue

Updated on

August 17, 2026

## Related articles

-   [
    
    ![How Blnk handles high-traffic hot balances](https://cdn.sanity.io/images/06pses5q/production/fa2035613e84c271b8deae8f0443488fc8b9431f-1792x1008.png?rect=140,0,1512,1008&w=1500&h=1000&fit=crop&auto=format)
    
    Build
    
    ### How Blnk handles high-traffic hot balances
    
    July 17, 2026
    
    ](https://blnkfinance.com/blog/how-blnk-handles-high-traffic-hot-balances)
-   [
    
    ![How to split payments for a delivery app across merchants, riders, and fees using the Blnk Ledger](https://cdn.sanity.io/images/06pses5q/production/0224ee04875ffed336bb60bf54bc2993a3f7a0a3-1792x1008.png?rect=140,0,1512,1008&w=1500&h=1000&fit=crop&auto=format)
    
    Build
    
    ### How to split payments for a delivery app across merchants, riders, and fees using the Blnk Ledger
    
    July 1, 2026
    
    ](https://blnkfinance.com/blog/how-to-split-payments-across-merchants-fees-and-revenue-with-blnk)
-   [
    
    ![How to build a loan disbursement and repayment app with Flutterwave + Blnk](https://cdn.sanity.io/images/06pses5q/production/a7bba474ff8e34e828ce6453ddde49a6457461ef-1920x1080.png?rect=150,0,1620,1080&w=1500&h=1000&fit=crop&auto=format)
    
    Build
    
    ### How to build a loan disbursement and repayment app with Flutterwave + Blnk
    
    April 23, 2026
    
    ](https://blnkfinance.com/blog/how-to-build-a-loan-disbursement-and-repayment-app-with-flutterwave-blnk)
