DEV Community

Cover image for A Practical Guide to Building Your First Card Payment System with Blnk Finance

A Practical Guide to Building Your First Card Payment System with Blnk Finance

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.

Blnk Store App


To build this you need the following:

  1. Node.js (v18 or higher) installed
  2. Docker installed and running
  3. 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.

ecommerce-demo-money-movement-map

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
}
Enter fullscreen mode Exit fullscreen mode
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"
}
Enter fullscreen mode Exit fullscreen mode
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"
  }
}
Enter fullscreen mode Exit fullscreen mode
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 value 95500000 means $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"
}
Enter fullscreen mode Exit fullscreen mode
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:

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
Enter fullscreen mode Exit fullscreen mode

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"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Install the Blnk CLI globally, which provides helpful commands for managing your Blnk instance:

npm i -g @blnkfinance/blnk-cli
Enter fullscreen mode Exit fullscreen mode

Verify Blnk is running by visiting: http://localhost:5001/ledgers

Browser showing empty ledgers JSON response


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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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 };
  }
}
Enter fullscreen mode Exit fullscreen mode

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 };
  }
}
Enter fullscreen mode Exit fullscreen mode

The status is set to pending
The status is set to pending


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 };
}
Enter fullscreen mode Exit fullscreen mode

There are two possible outcomes when settling a transaction.

If the merchant commits the transaction, its status becomes APPLIED and the money moves permanently from the customer to the merchant.
status applied
If the merchant voids the transaction, its status becomes VOID and all held funds are released back to the customer's available balance.
status void


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
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

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.

The user selects the products
Selected items

The order is then successfully placed
App showing

3. Merchant Settlement

Merchant dashboard showing Accept/Reject buttons
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
Enter fullscreen mode Exit fullscreen mode

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.

Resources


Top comments (0)