A step-by-step guide for developers to implement card payments with authorization and settlement flows
Introduction
Have you ever wondered how card payments actually work behind the scenes? When you swipe your card at a store, your money doesn't instantly leave your account. Instead, there's a fascinating two-step process: authorization (holding the funds) and settlement (actually moving the money).
In this guide, we'll show you how to build a fully functional e-commerce demo that implements this exact flow using Blnk Finance — an open-source ledger for building fintech products.
We'd demonstrate using a simple online store where users can create a virtual card, shop for products, and where merchants can accept or reject payments.
To build this you need the following:
- Node.js (v18 or higher) installed
- Docker installed and running
- Basic knowledge of TypeScript and React
If you'd like to see the live demo of the e-commerce store and a step-by-step code explanation, check out the video below:
Build A Card Payment System with Blnk Finance
Note: In the video above, I specifically focus on the Inflight Transaction log, demonstrating how Blnk ensures financial integrity during the authorization hold phase.
The Money Movement Map
Let's design how money will flow in our system before writing any code. The Money Movement Map is the blueprint for your financial application.
Funding comes from
@WorldUSD(external source) to the customer's card. When the customer pays, funds are held (not moved yet). Thereafter, when the merchant accepts → funds move permanently. Merchant rejects → funds released.
Next, let's understand some important terms:
1.Ledger: Think of it as a folder that contains all the balances for a specific purpose.
{
"ledger_id": "ldg_5885cfce-873f-46ec-a4ec-a2204d2fff3d",
"name": "Alice's Card Ledger",
"created_at": "2026-01-08T00:39:06.160464Z",
"meta_data": null
}
| Field | What it means |
|---|---|
ledger_id |
Unique identifier for this ledger (starts with ldg_) |
name |
Human-readable name you assign |
created_at |
When the ledger was created |
meta_data |
Optional custom data you can attach |
For example, if you're a bank, you might have separate ledgers for "Savings Accounts", "Card Accounts", "Loan Accounts", etc.
2.Identity: represents a customer or entity in your system.
{
"identity_id": "idt_0449e806-262c-48d4-b07a-6d45d24f2704",
"identity_type": "individual",
"first_name": "Alice",
"last_name": "User",
"email_address": "alice@gmail.com",
"category": "customer",
"nationality": "US",
"created_at": "2026-01-08T00:39:06.44318Z"
}
| Field | What it means |
|---|---|
identity_id |
Unique identifier (starts with idt_) |
identity_type |
individual or organization
|
first_name, last_name
|
Customer's name |
category |
Your custom categorization (e.g., "customer", "merchant") |
3.Balance: an actual account that holds money. It belongs to a ledger and can be linked to an identity.
{
"balance_id": "bln_51658b42-577c-4922-8d7e-8726e41230e3",
"balance": 95500000,
"credit_balance": 100000000,
"debit_balance": 4500000,
"inflight_balance": null,
"currency": "USD",
"ledger_id": "ldg_5885cfce-873f-46ec-a4ec-a2204d2fff3d",
"meta_data": {
"card_id": "card-1767832745473",
"card_scheme": "visa",
"last_4_digits": "1234",
"type": "virtual"
}
}
| Field | What it means |
|---|---|
balance_id |
Unique identifier (starts with bln_) |
balance |
Current available balance (in smallest unit, e.g., cents) |
credit_balance |
Total money received (credits) |
debit_balance |
Total money sent out (debits) |
inflight_balance |
Money currently held/reserved (pending transactions) |
currency |
The currency code (USD, EUR, etc.) |
meta_data |
Custom data (we store card details here) |
Blnk stores amounts as integers to avoid floating-point errors. With
precision: 100, the value95500000means$955.00(divide by 100 for dollars, then by 100 again for the precision multiplier).
4.Transaction: records money moving from one balance to another.
{
"transaction_id": "txn_d1ccdfe7-b944-4bf3-88f4-4fa5daa0087e",
"amount": 79900,
"precise_amount": 7990000,
"precision": 100,
"source": "bln_51658b42-577c-4922-8d7e-8726e41230e3",
"destination": "bln_612c7f52-7bb0-4a49-9251-90dd3ec6ef70",
"reference": "AUTH-1767841533907",
"currency": "USD",
"description": "Purchase of 2 items",
"status": "QUEUED",
"inflight": true,
"created_at": "2026-01-08T03:05:35.759002Z"
}
| Field | What it means |
|---|---|
transaction_id |
Unique identifier (starts with txn_) |
amount |
The transaction amount |
precise_amount |
Amount × precision (for exact calculations) |
source |
Balance ID money comes FROM |
destination |
Balance ID money goes TO |
reference |
Your unique reference (for idempotency) |
status |
Current state: QUEUED, INFLIGHT, APPLIED, VOID
|
inflight |
If true, this is a pending/held transaction |
5.Inflight Transactions: is Blnk's way of handling the authorization → settlement flow (i.e, your pending transactions).
When you set inflight: true the transaction is created but NOT finalized. The funds are reversed (held) from the source balance. The transaction then stays in INFLIGHT status. later, you can commit (finalize) or void (cancel) it
Transaction Status Flow:
For example,
⇒You swipe your card at a restaurant for $50
⇒Your bank authorizes (creates inflight transaction) — $50 is held
⇒Your available balance shows $50 less
⇒Hours later, the restaurant settles (commits) — money actually moves
⇒OR, if you cancel, the restaurant voids — $50 is released back
6.Internal Balances (@world): Balances starting with @ are internal balances. They represent external sources/destinations.
-
@WorldUSD: Represents money entering/leaving your system -
@Merchant: — Could represent a merchant account -
@Bank: Could represent your bank connection
These are automatically created when first used.
Setting Up the Project
Step 1: Set Up Blnk Core
First, we need to get Blnk Core running locally. Start by cloning the Blnk repository, which contains the core ledger system we'll be connecting to. After cloning, you'll need to create a blnk.json configuration file to set up your Blnk instance. This configuration file tells Blnk how to run, what port to use, database connections, and other important settings. Once configured, launch Blnk using Docker, which will start the ledger server on your local machine.
Clone the Blnk repository:
git clone https://github.com/blnkfinance/blnk.git
cd blnk
Set up your configuration by creating a blnk.json file in the repository root. This file configures how Blnk Core will run, including database connections, Redis settings, server port, and notification preferences:
{
"project_name": "Blnk",
"data_source": {
"dns": "postgres://postgres:password@postgres:5432/blnk?sslmode=disable"
},
"redis": {
"dns": "redis:6379"
},
"server": {
"domain": "blnk.io",
"ssl": false,
"ssl_email": "your-email@example.com",
"port": "5001"
},
"notification": {
"slack": {
"webhook_url": "https://hooks.slack.com"
}
}
}
This configuration sets up Blnk to use PostgreSQL for data storage, Redis for caching, and runs the server on port 5001.
Launch Blnk with Docker:
docker-compose up
Install the Blnk CLI globally, which provides helpful commands for managing your Blnk instance:
npm i -g @blnkfinance/blnk-cli
Verify Blnk is running by visiting: http://localhost:5001/ledgers
Step 2: Install Blnk TypeScript SDK and Create Next.js Project
Before creating our Next.js application, we need to install the Blnk TypeScript SDK, which provides all the functions we need to interact with Blnk Core.
First, install the Blnk TypeScript SDK. You can do this globally or we'll install it in the Next.js project directory in the next step:
First, install the Blnk TypeScript SDK. You can do this globally or we'll install it in the Next.js project directory in the next step:
npm install @blnkfinance/blnk-typescript
Now create the Next.js project with TypeScript support, Tailwind CSS for styling, and the App Router (Next.js 14's routing system). After creating the project, we'll install the Blnk TypeScript SDK in the project directory:
npx create-next-app@latest blnk-store --typescript --tailwind --app
cd blnk-store
npm install @blnkfinance/blnk-typescript
Step 3: Configure Environment Variables
The API key can be left empty if you're running Blnk locally without authentication. Create a .env.local file in your project root:
BLNK_API_KEY=
BLNK_CORE_URL=http://localhost:5001
Step 4: Initialize the Blnk SDK
This sets up a connection to Blnk Core that we'll reuse throughout our application. It uses a singleton pattern, which means we create the Blnk SDK instance once and reuse it for all API calls. This is more efficient than creating a new connection every time. Create src/lib/blnk.ts:
import { BlnkInit } from '@blnkfinance/blnk-typescript';
let blnkInstance: Awaited<ReturnType<typeof BlnkInit>> | null = null;
export async function getBlnkInstance() {
if (!blnkInstance) {
const baseUrl = (process.env.BLNK_CORE_URL || 'http://localhost:5001').replace(/\/$/, '');
blnkInstance = await BlnkInit(process.env.BLNK_API_KEY || '', {
baseUrl,
});
}
return blnkInstance;
}
Implementing the Backend (Server Actions)
Now let's implement the core functionality. Create src/app/actions.ts:
Creating a Card Account
This helps create four essential components in sequence: a ledger to organize the customer's accounts, an identity to represent the customer, a balance to serve as the actual card account, and a funding transaction to add initial money to the card. Each step depends on the previous one, so we create them in order and use the IDs returned from each step for the next:
'use server';
import { getBlnkInstance } from '@/lib/blnk';
export async function createCardAccount(name: string, email: string) {
const blnk = await getBlnkInstance();
const { Ledgers, Identity, LedgerBalances, Transactions } = blnk;
try {
// Step 1: Create a Ledger
console.log('Creating Ledger...');
const ledger = await Ledgers.create({
name: `${name}'s Card Ledger`,
});
const ledgerId = ledger.data.ledger_id;
console.log('Ledger created:', ledgerId);
// Step 2: Create an Identity
console.log('Creating Identity...');
const [firstName, lastName] = name.split(' ');
const identity = await Identity.create({
identity_type: "individual",
first_name: firstName || name,
last_name: lastName || 'User',
email_address: email,
phone_number: '+1234567890',
gender: 'other',
dob: new Date('1990-01-01'),
nationality: 'US',
category: 'customer',
street: '123 Main St',
country: 'US',
state: 'NY',
post_code: '10001',
city: 'New York',
});
const identityId = identity.data.identity_id;
console.log('Identity created:', identityId);
// Step 3: Create a Card Balance
console.log('Creating Card Balance...');
const balance = await LedgerBalances.create({
ledger_id: ledgerId,
identity_id: identityId,
currency: "USD",
meta_data: {
last_4_digits: "1234",
card_scheme: "visa",
type: "virtual",
card_id: `card-${Date.now()}`
}
});
const balanceId = balance.data.balance_id;
console.log('Card Balance created:', balanceId);
// Step 4: Fund the card with initial balance
console.log('Funding card...');
await Transactions.create({
amount: 1000000, // $10,000.00
precision: 100,
reference: `FUND-${Date.now()}`,
description: "Initial Funding",
currency: "USD",
source: "@WorldUSD",
destination: balanceId,
allow_overdraft: true
});
console.log('Card funded with $10,000');
return { success: true, ledgerId, identityId, balanceId };
} catch (error) {
console.error("Error:", error);
return { success: false, error: error.message };
}
}
This creates a ledger that acts as a container for this customer's accounts. Second, it creates an identity that represents the customer profile with their personal information. Third, it creates a balance that serves as the actual card account that will hold money. Finally, it creates a funding transaction that adds $10,000 from @WorldUSD, which represents an external source of money entering the system.
Authorizing a Payment (Creating Inflight Transaction)
When a customer completes checkout, we need to authorize the payment by creating an inflight transaction. T This tells Blnk to hold the funds without actually moving them yet, which is exactly how real card payments work during the authorization phase:
export async function authorizePayment(
cardBalanceId: string,
amount: number,
description: string
) {
const blnk = await getBlnkInstance();
const { Transactions } = blnk;
try {
const transaction = await Transactions.create({
amount: amount * 100, // Convert dollars to cents
precision: 100,
currency: "USD",
reference: `AUTH-${Date.now()}`,
source: cardBalanceId, // Customer's card
destination: "@WorldUSD", // Merchant
description: description,
inflight: true // This makes it a hold!
});
console.log('Payment authorized:', transaction.data.transaction_id);
return { success: true, transaction: transaction.data };
} catch (error) {
return { success: false, error: error.message };
}
}
Settling the Transaction (Commit or Void)
After a payment is authorized, the merchant needs to settle it by either accepting or rejecting the transaction.
We'd make a direct API call to Blnk Core's inflight transaction endpoint, sending either a "commit" or "void" action. When committing, the transaction status changes to APPLIED and the money moves permanently. When voiding, the transaction status changes to VOID, and the held funds are released back to the customer
export async function settleTransaction(
transactionId: string,
action: 'commit' | 'void'
) {
const baseUrl = process.env.BLNK_CORE_URL || 'http://localhost:5001';
const response = await fetch(
`${baseUrl}/transactions/inflight/${transactionId}`,
{
method: 'PUT',
headers: {
'X-blnk-key': process.env.BLNK_API_KEY || '',
'Content-Type': 'application/json'
},
body: JSON.stringify({ status: action })
}
);
const data = await response.json();
if (action === 'commit') {
console.log('Transaction committed - money moved permanently');
} else {
console.log('Transaction voided - funds released back to customer');
}
return { success: true, data };
}
There are two possible outcomes when settling a transaction.
If the merchant commits the transaction, its status becomes
APPLIEDand the money moves permanently from the customer to the merchant.
If the merchant voids the transaction, its status becomesVOIDand all held funds are released back to the customer's available balance.
Building the Frontend
Project Structure
src/
├── app/
│ ├── page.tsx # Main page
│ ├── actions.ts # Server actions (backend)
│ └── layout.tsx # App layout
├── components/
│ ├── CardSetup.tsx # Account creation form
│ ├── StoreView.tsx # Product catalog & cart
│ ├── MerchantDashboard.tsx # Settlement UI
│ ├── Navbar.tsx # Navigation
│ └── ui.tsx # Reusable UI components
└── lib/
├── blnk.ts # Blnk SDK initialization
└── store.tsx # React Context for state
Connecting Frontend to Backend
When the user clicks "Pay", the frontend calls the authorizePayment() server action. This server action then makes an API call to Blnk Core, creating a transaction with inflight: true to hold the funds. Once the transaction is successfully created, the frontend stores the transaction details in its state management system so it can be displayed in the merchant dashboard for settlement.
// In StoreView.tsx
const handleCheckout = async () => {
setLoading(true);
// Call the server action
const result = await authorizePayment(
user.balanceId,
total,
`Purchase of ${cart.length} items`
);
if (result.success && result.transaction) {
// Store the pending transaction
addTransaction({
id: result.transaction.transaction_id,
amount: total,
description: `Purchase of ${cart.length} items`,
status: 'INFLIGHT',
reference: result.transaction.reference,
timestamp: new Date().toISOString()
});
setSuccess(`Order authorized! Ref: ${result.transaction.reference}`);
clearCart();
}
setLoading(false);
};
Testing the Complete Flow
Let's walk through the entire user journey:
1. Create Account
The user journey begins when a new customer enters their name and email address in the card setup form. Behind the scenes, the system creates a complete account structure: first a ledger to organize their accounts, then an identity to represent the customer, followed by a balance that serves as their virtual card, and finally a funding transaction that adds $10,000 to their card. Once this process completes, the user has a fully functional virtual card with $10,000 ready to use.
2. Shop and Checkout
Users can browse the product catalog, add items to their shopping cart, and proceed to checkout. When they click the "Pay" button, the system creates an inflight transaction that holds the funds from their card balance.
3. Merchant Settlement

Once a payment is authorized, the merchant can view all pending orders in their dashboard. When the merchant clicks "Accept", the system commits the inflight transaction, which changes its status to APPLIED and permanently moves the money from the customer to the merchant. Alternatively, if the merchant clicks "Reject", the system voids the transaction, changing its status to VOID and releasing all held funds back to the customer's available balance.
4. Verify in Blnk
Check your Blnk Core directly:
http://localhost:5001/ledgers # See all ledgers
http://localhost:5001/balances # See all balances
http://localhost:5001/identities # See all identities
http://localhost:5001/transactions/{id} # See specific transaction
Summary
We've built a complete card payment system that demonstrates the core concepts of digital ledger-based payments. Your system includes ledgers that organize customer accounts, identities that represent customers, balances that serve as actual card accounts, inflight transactions that create authorization holds, and settlement functionality that allows merchants to commit or void pending payments.
Ready to dig deeper? Experiment with your project and see the magic for yourself.
If you have questions about how to build your specific use case with Blnk, feel free to reach out to us via Support or Contact Sales.









Top comments (0)