Skip to main content
Bulk transactions allow you to process hundreds or thousands of transactions in a single request. This is essential for payroll, mass payouts, dividend distributions, and batch refunds.

How bulk transactions work

When you submit a bulk transaction request:
  1. Blnk validates all transactions upfront
  2. Processes them in parallel using worker pools
  3. Supports both synchronous and asynchronous modes
  4. Provides atomic or non-atomic processing options
  5. Returns a batch ID for tracking

Basic bulk transaction

Process multiple transactions in a single request:
1

Submit bulk transaction request

POST /transactions/bulk
{
  "transactions": [
    {
      "amount": 100.00,
      "precision": 100,
      "reference": "payroll_emp_001",
      "currency": "USD",
      "source": "bln_company_payroll",
      "destination": "bln_employee_001",
      "description": "Monthly salary"
    },
    {
      "amount": 150.00,
      "precision": 100,
      "reference": "payroll_emp_002",
      "currency": "USD",
      "source": "bln_company_payroll",
      "destination": "bln_employee_002",
      "description": "Monthly salary"
    },
    {
      "amount": 120.00,
      "precision": 100,
      "reference": "payroll_emp_003",
      "currency": "USD",
      "source": "bln_company_payroll",
      "destination": "bln_employee_003",
      "description": "Monthly salary"
    }
  ]
}
2

Synchronous response

{
  "batch_id": "batch_abc123xyz",
  "status": "applied",
  "transaction_count": 3
}
All three transactions are processed immediately and the response confirms success.

Asynchronous processing

For large batches, process asynchronously to avoid timeouts:
1

Submit with async flag

POST /transactions/bulk
{
  "transactions": [
    // ... array of transactions
  ],
  "run_async": true
}
2

Immediate response

{
  "message": "Bulk transaction processing started",
  "batch_id": "batch_xyz789abc",
  "status": "processing"
}
The request returns immediately with status processing.
3

Check status via webhook

Set up a webhook to receive completion notifications:
{
  "event": "bulk_transaction.completed",
  "data": {
    "batch_id": "batch_xyz789abc",
    "status": "applied",
    "transaction_count": 1000,
    "completed_at": "2024-01-15T10:45:00Z"
  }
}

Atomic bulk transactions

Ensure all transactions succeed or none are applied:
POST /transactions/bulk
{
  "transactions": [
    {
      "amount": 50.00,
      "precision": 100,
      "reference": "batch_refund_001",
      "currency": "USD",
      "source": "bln_merchant_wallet",
      "destination": "bln_customer_001"
    },
    {
      "amount": 75.00,
      "precision": 100,
      "reference": "batch_refund_002",
      "currency": "USD",
      "source": "bln_merchant_wallet",
      "destination": "bln_customer_002"
    }
  ],
  "atomic": true
}
With atomic: true, if ANY transaction fails (e.g., insufficient balance), ALL transactions in the batch are rolled back.

Inflight bulk transactions

Create multiple authorization holds at once:
POST /transactions/bulk
{
  "transactions": [
    {
      "amount": 100.00,
      "precision": 100,
      "reference": "bulk_auth_001",
      "currency": "USD",
      "source": "bln_customer_wallet",
      "destination": "bln_merchant_wallet"
    },
    {
      "amount": 200.00,
      "precision": 100,
      "reference": "bulk_auth_002",
      "currency": "USD",
      "source": "bln_customer_wallet",
      "destination": "bln_merchant_wallet"
    }
  ],
  "inflight": true
}
All transactions in the batch will have status: "INFLIGHT".

Skip queue option

Process transactions immediately without queueing:
POST /transactions/bulk
{
  "transactions": [
    // ... transactions
  ],
  "skip_queue": true
}
Use skip_queue: true for high-priority transactions that need immediate processing. This bypasses the normal transaction queue.

Common use cases

Payroll processing

POST /transactions/bulk
{
  "transactions": [
    {
      "amount": 3500.00,
      "precision": 100,
      "reference": "payroll_jan_2024_emp_001",
      "currency": "USD",
      "source": "bln_company_payroll_account",
      "destination": "bln_employee_wallet_001",
      "description": "January 2024 salary",
      "meta_data": {
        "employee_id": "EMP001",
        "month": "2024-01",
        "payment_type": "salary"
      }
    },
    {
      "amount": 4200.00,
      "precision": 100,
      "reference": "payroll_jan_2024_emp_002",
      "currency": "USD",
      "source": "bln_company_payroll_account",
      "destination": "bln_employee_wallet_002",
      "description": "January 2024 salary",
      "meta_data": {
        "employee_id": "EMP002",
        "month": "2024-01",
        "payment_type": "salary"
      }
    }
    // ... more employees
  ],
  "run_async": true,
  "atomic": true
}

Mass refunds

POST /transactions/bulk
{
  "transactions": [
    {
      "amount": 29.99,
      "precision": 100,
      "reference": "refund_order_12345",
      "currency": "USD",
      "source": "bln_merchant_account",
      "destination": "bln_customer_wallet_001",
      "description": "Refund for order #12345",
      "meta_data": {
        "original_order_id": "12345",
        "refund_reason": "product_recall"
      }
    },
    {
      "amount": 49.99,
      "precision": 100,
      "reference": "refund_order_12346",
      "currency": "USD",
      "source": "bln_merchant_account",
      "destination": "bln_customer_wallet_002",
      "description": "Refund for order #12346",
      "meta_data": {
        "original_order_id": "12346",
        "refund_reason": "product_recall"
      }
    }
    // ... more refunds
  ],
  "run_async": true
}

Dividend distribution

POST /transactions/bulk
{
  "transactions": [
    {
      "amount": 150.00,
      "precision": 100,
      "reference": "dividend_q4_2024_investor_001",
      "currency": "USD",
      "source": "bln_company_dividend_pool",
      "destination": "bln_investor_wallet_001",
      "description": "Q4 2024 dividend payment",
      "meta_data": {
        "investor_id": "INV001",
        "shares": 100,
        "dividend_per_share": 1.50,
        "quarter": "Q4-2024"
      }
    },
    {
      "amount": 300.00,
      "precision": 100,
      "reference": "dividend_q4_2024_investor_002",
      "currency": "USD",
      "source": "bln_company_dividend_pool",
      "destination": "bln_investor_wallet_002",
      "description": "Q4 2024 dividend payment",
      "meta_data": {
        "investor_id": "INV002",
        "shares": 200,
        "dividend_per_share": 1.50,
        "quarter": "Q4-2024"
      }
    }
    // ... more investors
  ],
  "run_async": true,
  "skip_queue": false
}

Creator payouts

POST /transactions/bulk
{
  "transactions": [
    {
      "amount": 500.00,
      "precision": 100,
      "reference": "payout_creator_jan_001",
      "currency": "USD",
      "source": "bln_platform_payout_account",
      "destination": "bln_creator_wallet_001",
      "description": "January earnings payout",
      "meta_data": {
        "creator_id": "creator_001",
        "period": "2024-01",
        "total_views": 50000,
        "revenue_share": 0.70
      }
    },
    {
      "amount": 750.00,
      "precision": 100,
      "reference": "payout_creator_jan_002",
      "currency": "USD",
      "source": "bln_platform_payout_account",
      "destination": "bln_creator_wallet_002",
      "description": "January earnings payout",
      "meta_data": {
        "creator_id": "creator_002",
        "period": "2024-01",
        "total_views": 75000,
        "revenue_share": 0.70
      }
    }
    // ... more creators
  ],
  "run_async": true
}

Performance considerations

Batch size recommendations

Small batches (< 100)

Use synchronous mode for immediate results

Medium batches (100-1000)

Use async mode with run_async: true

Large batches (> 1000)

Split into multiple async requests

Atomic batches

Keep under 500 transactions for better performance

Worker pool configuration

Blnk processes bulk transactions using worker pools. The default configuration:
const (
    asyncBulkSemaphore = semaphore.NewWeighted(100) // max 100 concurrent
    asyncTxnSemaphore  = semaphore.NewWeighted(20)  // max 20 concurrent processors
)
From transaction.go:57-58

Error handling

Individual transaction failures (non-atomic)

With atomic: false, individual failures don’t stop the batch:
{
  "batch_id": "batch_123",
  "status": "applied",
  "transaction_count": 100,
  "failed_count": 3,
  "errors": [
    {
      "reference": "payroll_emp_042",
      "error": "insufficient balance"
    },
    {
      "reference": "payroll_emp_055",
      "error": "balance not found"
    },
    {
      "reference": "payroll_emp_089",
      "error": "reference already used"
    }
  ]
}

Atomic batch failure

With atomic: true, the entire batch fails if any transaction fails:
{
  "error": "transaction rejected",
  "batch_id": "batch_456",
  "failed_transaction": "payroll_emp_042",
  "reason": "insufficient balance"
}
All transactions in the batch are rolled back.

Implementation code (from api/transactions.go:456-493)

The bulk transaction handler:
func (a Api) CreateBulkTransactions(c *gin.Context) {
    var req model.BulkTransactionRequest
    if err := c.BindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body: " + err.Error()})
        return
    }

    // Call the service layer to handle bulk transaction creation
    result, err := a.blnk.CreateBulkTransactions(c.Request.Context(), &req)
    
    if err != nil {
        logrus.WithError(err).WithField("batch_id", result.BatchID).Error("bulk transaction API error")
        c.JSON(http.StatusBadRequest, gin.H{
            "error":    result.Error,
            "batch_id": result.BatchID,
        })
        return
    }

    if req.RunAsync {
        // Async request acknowledged
        c.JSON(http.StatusAccepted, gin.H{
            "message":  "Bulk transaction processing started",
            "batch_id": result.BatchID,
            "status":   result.Status,
        })
    } else {
        // Synchronous request completed
        c.JSON(http.StatusCreated, gin.H{
            "batch_id":          result.BatchID,
            "status":            result.Status,
            "transaction_count": result.TransactionCount,
        })
    }
}

Best practices

Unique references

Ensure each transaction has a unique reference to prevent duplicates

Use async for scale

Enable run_async for batches over 100 transactions

Implement webhooks

Set up webhooks to track async batch completion

Monitor batch IDs

Store batch IDs for tracking and reconciliation

Validate upfront

Verify balances and data before submitting large batches

Handle failures gracefully

Implement retry logic for failed transactions

Troubleshooting

Batch timeout

Problem: Large synchronous batches timing out Solution: Use run_async: true or split into smaller batches
{
  "run_async": true
}

Insufficient balance for atomic batch

Problem: Atomic batch fails due to one insufficient balance Solution: Pre-validate all balances before submitting:
for each transaction:
  GET /balances/{balance_id}
  verify balance >= transaction.amount

Duplicate references

Problem: Some transactions fail with “reference already used” Solution: Generate unique references with timestamps or UUIDs:
const reference = `payroll_${employeeId}_${Date.now()}`;

Next steps

Webhooks

Set up notifications for batch completion

Reconciliation

Match bulk transactions with external records

Build docs developers (and LLMs) love