Contents

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 usually include an opening 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.

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.

Let’s get started.

Getting started

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.

  • 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.
  • 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 to get you started.
  • Your DB credentials to connect and make queries from your code.

Step 1: Get the list of transactions

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.

```

--CODE language-bash--

curl -X POST 'http://localhost:5001/search/transactions' \
-H 'X-Blnk-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"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
}'

```

  • 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.

Your response should give you the following:

```

--CODE language-json--

{
 "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. 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.

```

--CODE language-bash--

curl -X GET 'http://localhost:5001/balances/{balance_id}/at?timestamp={iso_timestamp}' \
-H 'X-Blnk-Key: BLNK_API_KEY' \
-H 'Content-Type: application/json'

```

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

Example response:

```

--CODE language-json--

{
 "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.

```

--CODE language-bash--

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.

```

--CODE language-bash--

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: Apply formatting to make it user-friendly

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.

```

--CODE language-bash--

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.

```

--CODE language-bash--

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.

```

--CODE language-bash--

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):

```

--CODE language-bash--

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.

```

--CODE language-bash--

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: Putting 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.

```

--CODE language-bash--

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:

```

--CODE language-json--

{
"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.