<!-- Source: https://blnkfinance.com/blog/how-to-build-a-bulk-payroll-system-using-the-blnk-ledger -->

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

Feb 10, 2026 5 min read

# How to Build a Bulk Payroll System Using the Blnk Ledger API

-   Emmanuella Etop-Essien Developer Relations

![How to build a bulk payroll system using the Blnk Ledger API](https://cdn.sanity.io/images/06pses5q/production/a3e5a2bfecf3e3a8d9724a810fca93a3821ca38c-1920x1080.webp?w=1600&fit=max&auto=format)

On this page

1.  [Prerequisites](#prerequisites)
2.  [Set up your multi-currency payroll ledger](#set-up-your-multi-currency-payroll-ledger)
3.  [Fund your payroll account](#fund-your-payroll-account)
4.  [Process bulk payroll payments](#process-bulk-payroll-payments)
5.  [Synchronous bulk payment processing](#synchronous-bulk-payment-processing)
6.  [Async bulk payment processing](#async-bulk-payment-processing)
7.  [Retrieve all transactions in a payroll batch](#retrieve-all-transactions-in-a-payroll-batch)
8.  [Async bulk transactions](#async-bulk-transactions)
9.  [Synchronous transactions](#synchronous-transactions)

When building a bulk payout system that handles hundreds or thousands of transfers, you want to make sure transactions are processed completely, and everyone’s balances are updated appropriately. Whether it's multi-currency payroll, bulk disbursements, or mass vendor payments, getting batch payment processing right from the start saves you from costly reconciliation issues later.

If you try to handle each payment one by one, things get messy fast. You'll end up with systems that break easily and recovery processes that are way too complicated.

When implementing bulk payouts developers face various integration complexities. You would have to deal with complexities such as API rate limits when mass requests are throttled.

In this guide, you’ll learn how to handle bulk payouts using Blnk ledger. The same bulk transactions API also works for [loan disbursements and scheduled repayments](https://blnkfinance.com/blog/how-to-build-loan-disbursement-interest-and-repayments-with-blnk).

## \# Prerequisites

Before we start, make sure you have:

-   A running instance of Blnk. If you're setting up for the first time, our [best practices guide](https://blnkfinance.com/blog/building-with-blnk-ledger-best-practices-guide) walks you through how to structure your ledger correctly.
-   Optionally, sign up to [Blnk Cloud](https://blnkfinance.com/products/cloud) to manage your data as you build.

## \# Set up your multi-currency payroll ledger

First things first, we need to set up our ledger architecture. This is how ledgers and balances are structured in our Core for easier reference.

For this article, we’ll design an internal payroll ledger for **Acme Inc.,** a 3,000-person team with employees in 12 countries. Our ledger must have the following:

-   **Multi-currency:** We’ll be paying out to 12 different countries with different tax laws and we may need to track them.
-   **Payslip generation:** Every employee must own a balance to track their entire payroll history within the company.
-   **Internal accounting/auditability:** The Acme Finance team must be able to track and reconcile money movement on the company accounts.

To implement this in our ledger:

1.  **Create ledgers per country:** This allows us to group employees from a particular country in one place, e.g., USA Ledger for all US-based employees, ITA Ledger for all Italy-based employees, and so on. [Read docs](https://docs.blnkfinance.com/ledgers/introduction).
2.  **Create an [identity for each employee](https://blnkfinance.com/glossary/identity):** Record every employee as an entity in the ledger. This will be important for the next step. [Read docs](https://docs.blnkfinance.com/ledgers/introduction).
3.  **Create a balance for each employee:** Using the employee’s identity and their respective country of residence, create a balance for each employee. [Read docs](https://docs.blnkfinance.com/ledgers/introduction).

## \# Fund your payroll account

Now that you’re set up, let’s work on payments.

Before we process payouts to employees, we need to first fund a company payroll balance. This sets a clear payroll budget for the period and gives Blnk a limit to work with. As a result, each payout is validated against the available payroll balance, allowing Blnk to track/manage cases where requested payouts exceed the payroll balance.

To do this, we’ll fund a balance dedicated to Acme’s payroll in our ledger. We’ll call it “`@PayrollBalance`.” Read docs on [internal balances](https://docs.blnkfinance.com/balances/internal-balances).

![](https://cdn.sanity.io/images/06pses5q/production/030d0aa3c01fecc1a4579fe99076abd32c3fa77f-1920x639.png?w=1920&fit=max&auto=format)

Internal balances are balances owned by Acme, and we reference this using indicators (like @BusinessAccount) instead of making use of balance IDs in your transaction request.

To fund the payroll account, we’ll create a transaction to represent a deposit from the business’ bank account to their payroll balance in your product:

<!-- TypeScript -->
```
const response = await blnk.Transactions.create({
  amount: 2000000,
  precision: 100,
  reference: 'business_funding_2025-01-15',
  currency: 'USD',
  source: '@BankDeposits',
  destination: '@PayrollBalance',
  description: 'Payroll funding for January 2025',
  allow_overdraft: true,
});
```

<!-- Go -->
```
transaction, resp, err := client.Transaction.Create(
  blnkgo.CreateTransactionRequest{
    ParentTransaction: blnkgo.ParentTransaction{
      Amount: 2000000,
      Precision: 100,
      Reference: "business_funding_2025-01-15",
      Currency: "USD",
      Source: "@BankDeposits",
      Destination: "@PayrollBalance",
      Description: "Payroll funding for January 2025",
    },
    AllowOverdraft: true,
  },
)
```

<!-- Python -->
```
response = blnk.transactions.create({
  "amount": 2000000,
  "precision": 100,
  "reference": "business_funding_2025-01-15",
  "currency": "USD",
  "source": "@BankDeposits",
  "destination": "@PayrollBalance",
  "description": "Payroll funding for January 2025",
  "allow_overdraft": True,
})
```

<!-- Java -->
```
ApiResponse<JsonNode> response = blnk.transactions().create(
  CreateTransactions.create()
    .amount(2000000)
    .precision(100)
    .reference("business_funding_2025-01-15")
    .currency("USD")
    .source("@BankDeposits")
    .destination("@PayrollBalance")
    .description("Payroll funding for January 2025")
    .allowOverdraft(true));
```

In our example, we deposited $2,000,000.00 into Acme’s payroll balance. We used `@BankDeposits` as our source to ensure [double-entry.](https://blnkfinance.com/blog/double-entry-for-developers)

Our `@BankDeposits` balance here has been automatically created by Blnk in our [general ledger](https://blnkfinance.com/glossary/general-ledger) because it was specified with “`@`”

![](https://cdn.sanity.io/images/06pses5q/production/7deec754f847ab3671369cbd4ee14bc1d157cb2e-1365x523.png?w=1365&fit=max&auto=format)

Always calculate the total payroll amount and fund accordingly. Make sure to fund enough to cover all employee salaries.

## \# Process bulk payroll payments

Now, it’s that time of the month! Let’s payout 3,000 employees.

We can do this in two ways:

1.  **Synchronous processing:** Process everything in one go, and return a success/failed response.
2.  **Async processing:** Process all transactions in the background, and notify via webhooks/events.

## \# Synchronous bulk payment processing

With this, your batch payment would be processed in a single request. For Acme, we can process bulk payouts in batches of 100, i.e. a total of 30 requests to complete processing.

Now, we’ll create the bulk transaction request. You can create an array of transactions—one for each employee—and send them all in a single request.

First, let's prepare the transaction for a few employees:

<!-- TypeScript -->
```
const response = await blnk.Transactions.createBulk({
  run_async: false,
  transactions: [
    {
      amount: 8752,
      precision: 100,
      reference: 'PAYROLL_EMP150_2025-01-15',
      description: 'Salary payment for Stacy Jones - 2025-01-15',
      currency: 'USD',
      source: '@BusinessAccount',
      destination: 'bln_c37fa6ca-b430-48f1-9095-1ebc514589d6',
      allow_overdraft: false,
      meta_data: {
        employee_id: 'EMP150',
        employee_name: 'Stacy Jones',
        payroll_period: '2025-01-15',
        payment_type: 'salary',
      },
    },
    {
      amount: 7243,
      precision: 100,
      reference: 'PAYROLL_EMP149_2025-01-15',
      description: 'Salary payment for Gene Wells - 2025-01-15',
      currency: 'USD',
      source: '@BusinessAccount',
      destination: 'bln_6c99439c-99e1-4f9b-923b-493693f38dff',
      allow_overdraft: false,
      meta_data: {
        employee_id: 'EMP149',
        employee_name: 'Gene Wells',
        payroll_period: '2025-01-15',
        payment_type: 'salary',
      },
    },
  ],
});
```

<!-- Go -->
```
result, resp, err := client.Transaction.CreateBulk(blnkgo.CreateBulkTransactionRequest{
  RunAsync: false,
  Transactions: []blnkgo.CreateTransactionRequest{
    blnkgo.CreateTransactionRequest{
      ParentTransaction: blnkgo.ParentTransaction{
        Amount: 8752,
        Precision: 100,
        Reference: "PAYROLL_EMP150_2025-01-15",
        Description: "Salary payment for Stacy Jones - 2025-01-15",
        Currency: "USD",
        Source: "@BusinessAccount",
        Destination: "bln_c37fa6ca-b430-48f1-9095-1ebc514589d6",
        MetaData: blnkgo.MetaData{
          "employee_id": "EMP150",
          "employee_name": "Stacy Jones",
          "payroll_period": "2025-01-15",
          "payment_type": "salary",
        },
      },
      AllowOverdraft: false,
    },
    blnkgo.CreateTransactionRequest{
      ParentTransaction: blnkgo.ParentTransaction{
        Amount: 7243,
        Precision: 100,
        Reference: "PAYROLL_EMP149_2025-01-15",
        Description: "Salary payment for Gene Wells - 2025-01-15",
        Currency: "USD",
        Source: "@BusinessAccount",
        Destination: "bln_6c99439c-99e1-4f9b-923b-493693f38dff",
        MetaData: blnkgo.MetaData{
          "employee_id": "EMP149",
          "employee_name": "Gene Wells",
          "payroll_period": "2025-01-15",
          "payment_type": "salary",
        },
      },
      AllowOverdraft: false,
    },
  },
})
```

<!-- Python -->
```
response = blnk.transactions.create_bulk({
  "run_async": False,
  "transactions": [
    {
      "amount": 8752,
      "precision": 100,
      "reference": "PAYROLL_EMP150_2025-01-15",
      "description": "Salary payment for Stacy Jones - 2025-01-15",
      "currency": "USD",
      "source": "@BusinessAccount",
      "destination": "bln_c37fa6ca-b430-48f1-9095-1ebc514589d6",
      "allow_overdraft": False,
      "meta_data": {
        "employee_id": "EMP150",
        "employee_name": "Stacy Jones",
        "payroll_period": "2025-01-15",
        "payment_type": "salary",
      },
    },
    {
      "amount": 7243,
      "precision": 100,
      "reference": "PAYROLL_EMP149_2025-01-15",
      "description": "Salary payment for Gene Wells - 2025-01-15",
      "currency": "USD",
      "source": "@BusinessAccount",
      "destination": "bln_6c99439c-99e1-4f9b-923b-493693f38dff",
      "allow_overdraft": False,
      "meta_data": {
        "employee_id": "EMP149",
        "employee_name": "Gene Wells",
        "payroll_period": "2025-01-15",
        "payment_type": "salary",
      },
    },
  ],
})
```

<!-- Java -->
```
ApiResponse<JsonNode> response = blnk.transactions().createBulk(
  CreateBulkTransactions.create()
    .runAsync(false)
    .transactions(List.of(
    Map.ofEntries(
      Map.entry("amount", 8752),
      Map.entry("precision", 100),
      Map.entry("reference", "PAYROLL_EMP150_2025-01-15"),
      Map.entry("description", "Salary payment for Stacy Jones - 2025-01-15"),
      Map.entry("currency", "USD"),
      Map.entry("source", "@BusinessAccount"),
      Map.entry("destination", "bln_c37fa6ca-b430-48f1-9095-1ebc514589d6"),
      Map.entry("allow_overdraft", false),
      Map.entry("meta_data", Map.of("employee_id", "EMP150", "employee_name", "Stacy Jones", "payroll_period", "2025-01-15", "payment_type", "salary"))),
      Map.ofEntries(
        Map.entry("amount", 7243),
        Map.entry("precision", 100),
        Map.entry("reference", "PAYROLL_EMP149_2025-01-15"),
        Map.entry("description", "Salary payment for Gene Wells - 2025-01-15"),
        Map.entry("currency", "USD"),
        Map.entry("source", "@BusinessAccount"),
        Map.entry("destination", "bln_6c99439c-99e1-4f9b-923b-493693f38dff"),
        Map.entry("allow_overdraft", false),
        Map.entry("meta_data", Map.of("employee_id", "EMP149", "employee_name", "Gene Wells", "payroll_period", "2025-01-15", "payment_type", "salary"))))));
```

`run_async: false` tells Blnk that the transactions should be processed synchronously. For larger batches, the request may take longer to complete.

<!-- 200 OK -->
```
{
  "batch_id": "bulk_32472ce2-0ba2-46c1-b796-aafc6cd31811",
  "status": "applied",
  "transaction_count": 2
}
```

Store or use the `batch_id` to reference this batch in other operations.

## \# Async bulk payment processing

With async processing, `run_async: true`, Blnk accepts the request and returns a response telling you that it is being processed in the background.

With this, we can pass all 3,000 transactions in our payload without worrying about server timeouts:

<!-- TypeScript -->
```
const response = await blnk.Transactions.createBulk({
  run_async: true,
  transactions: [
    {
      amount: 5000,
      precision: 100,
      reference: 'PAYROLL_EMP001_2025-01-15',
      description: 'Salary payment for Employee 1 - 2025-01-15',
      currency: 'USD',
      source: '@BusinessAccount',
      destination: 'bln_employee_1_balance_id',
      allow_overdraft: false,
      meta_data: {
        employee_id: 'EMP001',
        employee_name: 'Employee 1',
        payroll_period: '2025-01-15',
      },
    },
  ],
});
```

<!-- Go -->
```
result, resp, err := client.Transaction.CreateBulk(blnkgo.CreateBulkTransactionRequest{
  RunAsync: true,
  Transactions: []blnkgo.CreateTransactionRequest{
    blnkgo.CreateTransactionRequest{
      ParentTransaction: blnkgo.ParentTransaction{
        Amount: 5000,
        Precision: 100,
        Reference: "PAYROLL_EMP001_2025-01-15",
        Description: "Salary payment for Employee 1 - 2025-01-15",
        Currency: "USD",
        Source: "@BusinessAccount",
        Destination: "bln_employee_1_balance_id",
        MetaData: blnkgo.MetaData{
          "employee_id": "EMP001",
          "employee_name": "Employee 1",
          "payroll_period": "2025-01-15",
        },
      },
      AllowOverdraft: false,
    },
  },
})
```

<!-- Python -->
```
response = blnk.transactions.create_bulk({
  "run_async": True,
  "transactions": [
    {
      "amount": 5000,
      "precision": 100,
      "reference": "PAYROLL_EMP001_2025-01-15",
      "description": "Salary payment for Employee 1 - 2025-01-15",
      "currency": "USD",
      "source": "@BusinessAccount",
      "destination": "bln_employee_1_balance_id",
      "allow_overdraft": False,
      "meta_data": {
        "employee_id": "EMP001",
        "employee_name": "Employee 1",
        "payroll_period": "2025-01-15",
      },
    },
  ],
})
```

<!-- Java -->
```
ApiResponse<JsonNode> response = blnk.transactions().createBulk(
  CreateBulkTransactions.create()
    .runAsync(true)
    .transactions(List.of(
    Map.ofEntries(
      Map.entry("amount", 5000),
      Map.entry("precision", 100),
      Map.entry("reference", "PAYROLL_EMP001_2025-01-15"),
      Map.entry("description", "Salary payment for Employee 1 - 2025-01-15"),
      Map.entry("currency", "USD"),
      Map.entry("source", "@BusinessAccount"),
      Map.entry("destination", "bln_employee_1_balance_id"),
      Map.entry("allow_overdraft", false),
      Map.entry("meta_data", Map.of("employee_id", "EMP001", "employee_name", "Employee 1", "payroll_period", "2025-01-15"))))));
```

<!-- 200 OK -->
```
{
  "batch_id": "bulk_204fa664-7171-4c38-9533-cc1086063376",
  "message": "Bulk transaction processing started",
  "status": "processing"
}
```

Since it is async, a `batch_id` is returned immediately and the payments continues in the background.

To know when the transactions have been processed, set up webhooks to listen for [`transaction.applied`](https://docs.blnkfinance.com/webhooks/overview) events and use them to trigger bank payouts via your payment provider.

This way, you can ensure that the transaction is verified first in your ledger before money leaves your system.

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

Payroll payment made to multiple employees in one request

## \# Retrieve all transactions in a payroll batch

To retrieve all transactions that belong to the same batch, use the [Search API](https://docs.blnkfinance.com/search/typesense/introduction) and query by a shared key such as a `batch_id`.

## \# Async bulk transactions

Blnk automatically assigns a batch identifier and stores it in transaction metadata under `QUEUED_PARENT_TRANSACTION`. You can use this value to retrieve all transactions in the single batch.

<!-- TypeScript -->
```
const response = await blnk.Search.search(
  {
    q: 'bulk_c62f200b-905f-4983-a349-cadd279234aa',
    query_by: 'meta_data.QUEUED_PARENT_TRANSACTION',
  },
  'transactions',
);
```

<!-- Go -->
```
searchResult, resp, err := client.Search.SearchDocument(
  blnkgo.SearchParams{
    Q: "bulk_c62f200b-905f-4983-a349-cadd279234aa",
    QueryBy: "meta_data.QUEUED_PARENT_TRANSACTION",
  },
  blnkgo.Transactions,
)
```

<!-- Python -->
```
response = blnk.search.search(
  {
    "q": "bulk_c62f200b-905f-4983-a349-cadd279234aa",
    "query_by": "meta_data.QUEUED_PARENT_TRANSACTION",
  },
  "transactions",
)
```

<!-- Java -->
```
ApiResponse<JsonNode> response = blnk.search().search(
  SearchParams.create()
    .q("bulk_c62f200b-905f-4983-a349-cadd279234aa")
    .queryBy("meta_data.QUEUED_PARENT_TRANSACTION"),
  "transactions");
```

This is the recommended approach for queued or asynchronous processing, since grouping is handled automatically.

## \# Synchronous transactions

For synchronous processing where multiple transactions are created without a single `batch_id`, include your own shared identifier in the transaction metadata.

For example, you might add a value like `tag: payroll-jan-2026` to each transaction.

You can then retrieve all related transactions by querying that metadata field:

<!-- TypeScript -->
```
const response = await blnk.Search.search(
  {
    q: 'payroll-jan-2026',
    query_by: 'meta_data.tag',
  },
  'transactions',
);
```

<!-- Go -->
```
searchResult, resp, err := client.Search.SearchDocument(
  blnkgo.SearchParams{
    Q: "payroll-jan-2026",
    QueryBy: "meta_data.tag",
  },
  blnkgo.Transactions,
)
```

<!-- Python -->
```
response = blnk.search.search(
  {
    "q": "payroll-jan-2026",
    "query_by": "meta_data.tag",
  },
  "transactions",
)
```

<!-- Java -->
```
ApiResponse<JsonNode> response = blnk.search().search(
  SearchParams.create()
    .q("payroll-jan-2026")
    .queryBy("meta_data.tag"),
  "transactions");
```

This approach gives you full control over grouping when transactions are created synchronously or across different workflows.

Alternatively, you can filter on Cloud and save to view for easy reference and operations. You can also generate employee payslips and [payroll statements directly from your ledger data](https://blnkfinance.com/blog/how-to-generate-customer-statements-from-your-ledger-with-blnk-postgres).

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

A filtered Cloud dashboard (with saved view) for a bulk transaction.

You now have a working bulk payroll system. [Get started with Blnk](https://docs.blnkfinance.com/) to try it out yourself. If you have any questions about this guide, reach out on [Discord](https://discord.gg/7WNv94zPpx) or send us a message.

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