Transactions

Bulk Transactions

Record many transactions in one request.

The Bulk Transaction API enables you to process multiple transactions within a single request. It offers two processing options: atomic processing, where all transactions either succeed or fail as a unit, and independent processing, where each transaction is handled separately.

Additionally, the API supports asynchronous processing to efficiently manage large batches of transactions.

To preview a batch without writing anything, set dry_run to true. See Dry-run transactions.

Creating bulk transactions

Configure and submit the batch

Call the Bulk Transactions API.

curl -X POST "http://localhost:5001/transactions/bulk" \
  -H "X-blnk-key: <api-key>" \
  -H "Content-Type: application/json" \
  -d '{
    "atomic": true,
    "inflight": false,
    "run_async": false,
    "skip_queue": false,
    "transactions": [
      {
        "precise_amount": 35890,
        "precision": 100,
        "reference": "unique_reference_1",
        "description": "Transaction description",
        "currency": "NGN",
        "source": "@source_account",
        "allow_overdraft": true,
        "destination": "@destination_account"
      },
      {
        "precise_amount": 35890,
        "precision": 100,
        "reference": "unique_reference_2",
        "description": "Transaction description",
        "currency": "NGN",
        "source": "@source_account",
        "allow_overdraft": true,
        "destination": "@destination_account"
      }
    ]
  }'
const response = await blnk.Transactions.createBulk({
  atomic: true,
  inflight: false,
  run_async: false,
  skip_queue: false,
  transactions: [
    {
      precise_amount: 35890,
      precision: 100,
      reference: 'unique_reference_1',
      description: 'Transaction description',
      currency: 'NGN',
      source: '@source_account',
      allow_overdraft: true,
      destination: '@destination_account',
    },
    {
      precise_amount: 35890,
      precision: 100,
      reference: 'unique_reference_2',
      description: 'Transaction description',
      currency: 'NGN',
      source: '@source_account',
      allow_overdraft: true,
      destination: '@destination_account',
    },
  ],
});
batch, _, _ := client.Transaction.CreateBulk(blnkgo.CreateBulkTransactionRequest{
    Atomic: true,
    Inflight: false,
    RunAsync: false,
    SkipQueue: false,
    Transactions: []blnkgo.CreateTransactionRequest{
        {
            PreciseAmount: big.NewInt(35890),
            Precision: 100,
            Reference: "unique_reference_1",
            Description: "Transaction description",
            Currency: "NGN",
            Source: "@source_account",
            Destination: "@destination_account",
            AllowOverdraft: true,
        },
        {
            PreciseAmount: big.NewInt(35890),
            Precision: 100,
            Reference: "unique_reference_2",
            Description: "Transaction description",
            Currency: "NGN",
            Source: "@source_account",
            Destination: "@destination_account",
            AllowOverdraft: true,
        },
    },
})
response = blnk.transactions.create_bulk({
  "atomic": True,
  "inflight": False,
  "run_async": False,
  "skip_queue": False,
  "transactions": [
    {
      "precise_amount": 35890,
      "precision": 100,
      "reference": "unique_reference_1",
      "description": "Transaction description",
      "currency": "NGN",
      "source": "@source_account",
      "allow_overdraft": True,
      "destination": "@destination_account",
    },
    {
      "precise_amount": 35890,
      "precision": 100,
      "reference": "unique_reference_2",
      "description": "Transaction description",
      "currency": "NGN",
      "source": "@source_account",
      "allow_overdraft": True,
      "destination": "@destination_account",
    },
  ],
})
ApiResponse<JsonNode> response = blnk.transactions().createBulk(
    BulkTransactions.create()
        .atomic(true)
        .inflight(false)
        .runAsync(false)
        .skipQueue(false)
        .transactions(List.of(
            CreateTransactions.create()
            .preciseAmount(35890)
            .precision(100)
            .reference("unique_reference_1")
            .description("Transaction description")
            .currency("NGN")
            .source("@source_account")
            .allowOverdraft(true)
            .destination("@destination_account"),
            CreateTransactions.create()
            .preciseAmount(35890)
            .precision(100)
            .reference("unique_reference_2")
            .description("Transaction description")
            .currency("NGN")
            .source("@source_account")
            .allowOverdraft(true)
            .destination("@destination_account")
        )));
FieldDescription
atomicWhen true, either all transactions succeed or all fail. When false, transactions are processed independently.
inflightWhen true, transactions are created in INFLIGHT status and require a separate commit. When false, transactions are processed immediately.
run_asyncWhen true, processing happens in the background and results are delivered via webhook. When false or not provided, processing happens synchronously and results are returned in the response.
skip_queueDefaults to false. When true, transactions bypass the queue and are processed inline in the request. Affects duplicate-reference handling - see Duplicate references.
transactionsArray of transaction objects. Max of 10,000 transactions per request.

Confirm the response

Blnk returns a batch_id that identifies the batch. When skip_queue: false, Blnk stores batch_id as the queued parent transaction.

Save it. You can use the same ID to retrieve, commit, refund, or void every transaction in the batch without tracking individual child IDs first. See Retrieving transactions in a batch.

When run_async is false:

{
  "batch_id": "bulk_c62f200b-905f-4983-a349-cadd279234aa",
  "status": "applied",
  "transaction_count": 2
}

When run_async is true, Blnk accepts the batch and returns immediately with status: "processing".

Processing continues in the background. The HTTP response does not wait for each transaction to finish.

Once done, Blnk sends one of the following webhook events with the final outcome.

  • bulk_transaction.applied
  • bulk_transaction.inflight
  • bulk_transaction.failed

See Webhook notifications.

{
  "batch_id": "bulk_c62f200b-905f-4983-a349-cadd279234aa",
  "status": "processing",
  "message": "Bulk transaction processing started"
}

Retrieving transactions in a batch

When you submit a bulk transaction request, Blnk assigns each transaction in the batch its own unique transaction ID and links it to your batch_id:

  • skip_queue: false (default): child transactions record batch_id in meta_data.QUEUED_PARENT_TRANSACTION.
  • skip_queue: true: child transactions set parent_transaction to batch_id.

Filter on meta_data.QUEUED_PARENT_TRANSACTION using Search via database filtering:

curl -X POST "http://localhost:5001/transactions/filter" \
  -H "Content-Type: application/json" \
  -H "X-blnk-key: <api-key>" \
  -d '{
    "filters": [
      {
        "field": "meta_data.QUEUED_PARENT_TRANSACTION",
        "operator": "eq",
        "value": "<batch-id>"
      }
    ]
  }'
const response = await blnk.Search.filter(
  {
    filters: [
      {
        field: 'meta_data.QUEUED_PARENT_TRANSACTION',
        operator: 'eq',
        value: '<batch-id>',
      },
    ],
  },
  'transactions',
);
results, _, _ := client.Transaction.Filter(blnkgo.FilterParams{
    Filters: []blnkgo.Filter{
        {
            Field: "meta_data.QUEUED_PARENT_TRANSACTION",
            Operator: blnkgo.OpEqual,
            Value: "<batch-id>",
        },
    },
})
response = blnk.search.filter(
  {
    "filters": [
      {
        "field": "meta_data.QUEUED_PARENT_TRANSACTION",
        "operator": "eq",
        "value": "<batch-id>",
      },
    ],
  },
  "transactions",
)
ApiResponse<JsonNode> response = blnk.search().filter(
    FilterParams.create()
        .filters(List.of(
            FilterCondition.create()
            .field("meta_data.QUEUED_PARENT_TRANSACTION")
            .operator("eq")
            .value("<batch-id>")
        )),
    "transactions");
{
  "data": [
    {
      "transaction_id": "txn_f482a1b3-6c2d-4e89-a17b-3d5e8f2a1c94",
      "meta_data": {
        "QUEUED_PARENT_TRANSACTION": "bulk_c62f200b-905f-4983-a349-cadd279234aa"
      },
      "status": "APPLIED"
    }
  ]
}

Filter on parent_transaction:

curl -X POST "http://localhost:5001/transactions/filter" \
  -H "Content-Type: application/json" \
  -H "X-blnk-key: <api-key>" \
  -d '{
    "filters": [
      {
        "field": "parent_transaction",
        "operator": "eq",
        "value": "<batch-id>"
      }
    ]
  }'
const response = await blnk.Search.filter(
  {
    filters: [
      {
        field: 'parent_transaction',
        operator: 'eq',
        value: '<batch-id>',
      },
    ],
  },
  'transactions',
);
results, _, _ := client.Transaction.Filter(blnkgo.FilterParams{
    Filters: []blnkgo.Filter{
        {
            Field: "parent_transaction",
            Operator: blnkgo.OpEqual,
            Value: "<batch-id>",
        },
    },
})
response = blnk.search.filter(
  {
    "filters": [
      {
        "field": "parent_transaction",
        "operator": "eq",
        "value": "<batch-id>",
      },
    ],
  },
  "transactions",
)
ApiResponse<JsonNode> response = blnk.search().filter(
    FilterParams.create()
        .filters(List.of(
            FilterCondition.create()
            .field("parent_transaction")
            .operator("eq")
            .value("<batch-id>")
        )),
    "transactions");
{
  "data": [
    {
      "transaction_id": "txn_f482a1b3-6c2d-4e89-a17b-3d5e8f2a1c94",
      "parent_transaction": "bulk_c62f200b-905f-4983-a349-cadd279234aa",
      "status": "APPLIED"
    }
  ]
}

Replace <batch-id> with the batch_id from the create response. Use the filter field that matches how you submitted the batch.

Handling inflight bulk transactions

Set inflight: true on the bulk request. Blnk ignores an inflight flag on child objects and holds every transaction in the batch. See Record bulk inflight transaction.

Commit or void the batch with batch_id. See Commit a split or a bulk batch and Void a split or a bulk batch.

Holds created one by one use Commit multiple inflights and Void many transactions.

Webhook notifications

When using run_async: true, the API sends webhook notifications upon completion or failure.

The webhooks follow this structure:

{
  "event": "bulk_transaction.applied",
  "data": {
    "batch_id": "bulk_c62f200b-905f-4983-a349-cadd279234aa",
    "status": "applied",
    "transaction_count": 4,       
    "error": "error message",     
    "timestamp": "2025-03-02T15:30:45+01:00"
  }
}
FieldTypeDescription
eventStringName of event. Can be bulk_transaction.applied, bulk_transaction.inflight, or bulk_transaction.failed.
batch_idStringSpecifies the id of the batch transaction.
statusStringStatus of the batch transaction. Can be applied, inflight, or failed.
transaction_countStringNumber of transactions in the batch. Only included for successful cases.
errorStringError message. Only included for failure cases.
timestampStringSpecifies the date & time when the batch transaction was completed.
{
  "event": "bulk_transaction.applied",
  "data": {
    "batch_id": "bulk_c62f200b-905f-4983-a349-cadd279234aa",
    "status": "applied",
    "transaction_count": 4,
    "timestamp": "2025-03-02T15:30:45+01:00"
  }
}
{
  "event": "bulk_transaction.failed",
  "data": {
    "batch_id": "bulk_4192d961-5b0e-46ca-bf2f-9386763057f8",
    "status": "failed",
    "error": "failed to queue transaction 2 (Reference: ref_967yg16hhh4q0t0dnsczvud9, Source: bln_a03ef6af-1e5d-46a8-86a9-5fb1f2286f66, Destination: bln_68be0aed-383c-4d27-87db-fb0650093686, Amount: 313333369.18): failed to apply transaction to balances: insufficient funds in source balance. All transactions in this batch have been refunded.",
    "timestamp": "2025-03-02T19:30:22.43597+01:00"
  }
}

Usage examples

Process multiple transactions atomically (atomic: true, run_async: false):

curl -X POST "http://localhost:5001/transactions/bulk" \
  -H "X-blnk-key: <api-key>" \
  -H "Content-Type: application/json" \
  -d '{
    "atomic": true,
    "inflight": false,
    "transactions": [
      {
        "precise_amount": 10000,
        "precision": 100,
        "reference": "tx_001",
        "description": "First transaction",
        "currency": "NGN",
        "source": "@account1",
        "destination": "@account2"
      },
      {
        "precise_amount": 5000,
        "precision": 100,
        "reference": "tx_002",
        "description": "Second transaction",
        "currency": "NGN",
        "source": "@account2",
        "destination": "@account3"
      }
    ]
  }'
const response = await blnk.Transactions.createBulk({
  atomic: true,
  inflight: false,
  transactions: [
    {
      precise_amount: 10000,
      precision: 100,
      reference: 'tx_001',
      description: 'First transaction',
      currency: 'NGN',
      source: '@account1',
      destination: '@account2',
    },
    {
      precise_amount: 5000,
      precision: 100,
      reference: 'tx_002',
      description: 'Second transaction',
      currency: 'NGN',
      source: '@account2',
      destination: '@account3',
    },
  ],
});
batch, _, _ := client.Transaction.CreateBulk(blnkgo.CreateBulkTransactionRequest{
    Atomic: true,
    Inflight: false,
    Transactions: []blnkgo.CreateTransactionRequest{
        {
            PreciseAmount: big.NewInt(10000),
            Precision: 100,
            Reference: "tx_001",
            Description: "First transaction",
            Currency: "NGN",
            Source: "@account1",
            Destination: "@account2",
        },
        {
            PreciseAmount: big.NewInt(5000),
            Precision: 100,
            Reference: "tx_002",
            Description: "Second transaction",
            Currency: "NGN",
            Source: "@account2",
            Destination: "@account3",
        },
    },
})
response = blnk.transactions.create_bulk({
  "atomic": True,
  "inflight": False,
  "transactions": [
    {
      "precise_amount": 10000,
      "precision": 100,
      "reference": "tx_001",
      "description": "First transaction",
      "currency": "NGN",
      "source": "@account1",
      "destination": "@account2",
    },
    {
      "precise_amount": 5000,
      "precision": 100,
      "reference": "tx_002",
      "description": "Second transaction",
      "currency": "NGN",
      "source": "@account2",
      "destination": "@account3",
    },
  ],
})
ApiResponse<JsonNode> response = blnk.transactions().createBulk(
    BulkTransactions.create()
        .atomic(true)
        .inflight(false)
        .transactions(List.of(
            CreateTransactions.create()
            .preciseAmount(10000)
            .precision(100)
            .reference("tx_001")
            .description("First transaction")
            .currency("NGN")
            .source("@account1")
            .destination("@account2"),
            CreateTransactions.create()
            .preciseAmount(5000)
            .precision(100)
            .reference("tx_002")
            .description("Second transaction")
            .currency("NGN")
            .source("@account2")
            .destination("@account3")
        )));
{
  "batch_id": "bulk_4192d961-5b0e-46ca-bf2f-9386763057f8",
  "status": "applied",
  "transaction_count": 2
}

Process a large batch in the background (run_async: true). Blnk returns immediately; the final outcome arrives via webhook.

curl -X POST "http://localhost:5001/transactions/bulk" \
  -H "X-blnk-key: <api-key>" \
  -H "Content-Type: application/json" \
  -d '{
    "atomic": true,
    "inflight": false,
    "run_async": true,
    "transactions": [
      {
        "precise_amount": 10000,
        "precision": 100,
        "reference": "tx_001",
        "description": "First transaction",
        "currency": "NGN",
        "source": "@account1",
        "destination": "@account2"
      },
      {
        "precise_amount": 7500,
        "precision": 100,
        "reference": "tx_999",
        "description": "Last transaction",
        "currency": "NGN",
        "source": "@account5",
        "destination": "@account6"
      }
    ]
  }'
const response = await blnk.Transactions.createBulk({
  atomic: true,
  inflight: false,
  run_async: true,
  transactions: [
    {
      precise_amount: 10000,
      precision: 100,
      reference: 'tx_001',
      description: 'First transaction',
      currency: 'NGN',
      source: '@account1',
      destination: '@account2',
    },
    {
      precise_amount: 7500,
      precision: 100,
      reference: 'tx_999',
      description: 'Last transaction',
      currency: 'NGN',
      source: '@account5',
      destination: '@account6',
    },
  ],
});
batch, _, _ := client.Transaction.CreateBulk(blnkgo.CreateBulkTransactionRequest{
    Atomic: true,
    Inflight: false,
    RunAsync: true,
    Transactions: []blnkgo.CreateTransactionRequest{
        {
            PreciseAmount: big.NewInt(10000),
            Precision: 100,
            Reference: "tx_001",
            Description: "First transaction",
            Currency: "NGN",
            Source: "@account1",
            Destination: "@account2",
        },
        {
            PreciseAmount: big.NewInt(7500),
            Precision: 100,
            Reference: "tx_999",
            Description: "Last transaction",
            Currency: "NGN",
            Source: "@account5",
            Destination: "@account6",
        },
    },
})
response = blnk.transactions.create_bulk({
  "atomic": True,
  "inflight": False,
  "run_async": True,
  "transactions": [
    {
      "precise_amount": 10000,
      "precision": 100,
      "reference": "tx_001",
      "description": "First transaction",
      "currency": "NGN",
      "source": "@account1",
      "destination": "@account2",
    },
    {
      "precise_amount": 7500,
      "precision": 100,
      "reference": "tx_999",
      "description": "Last transaction",
      "currency": "NGN",
      "source": "@account5",
      "destination": "@account6",
    },
  ],
})
ApiResponse<JsonNode> response = blnk.transactions().createBulk(
    BulkTransactions.create()
        .atomic(true)
        .inflight(false)
        .runAsync(true)
        .transactions(List.of(
            CreateTransactions.create()
            .preciseAmount(10000)
            .precision(100)
            .reference("tx_001")
            .description("First transaction")
            .currency("NGN")
            .source("@account1")
            .destination("@account2"),
            CreateTransactions.create()
            .preciseAmount(7500)
            .precision(100)
            .reference("tx_999")
            .description("Last transaction")
            .currency("NGN")
            .source("@account5")
            .destination("@account6")
        )));
{
  "batch_id": "bulk_c62f200b-905f-4983-a349-cadd279234aa",
  "status": "processing",
  "message": "Bulk transaction processing started"
}

Create inflight batch

Set inflight: true on the bulk request. See Handling inflight bulk transactions.

curl -X POST "http://localhost:5001/transactions/bulk" \
  -H "X-blnk-key: <api-key>" \
  -H "Content-Type: application/json" \
  -d '{
    "atomic": true,
    "inflight": true,
    "transactions": [
      {
        "precise_amount": 10000,
        "precision": 100,
        "reference": "tx_003",
        "description": "First transaction",
        "currency": "NGN",
        "source": "@account1",
        "destination": "@account2"
      },
      {
        "precise_amount": 5000,
        "precision": 100,
        "reference": "tx_004",
        "description": "Second transaction",
        "currency": "NGN",
        "source": "@account2",
        "destination": "@account3"
      }
    ]
  }'
const response = await blnk.Transactions.createBulk({
  atomic: true,
  inflight: true,
  transactions: [
    {
      precise_amount: 10000,
      precision: 100,
      reference: 'tx_003',
      description: 'First transaction',
      currency: 'NGN',
      source: '@account1',
      destination: '@account2',
    },
    {
      precise_amount: 5000,
      precision: 100,
      reference: 'tx_004',
      description: 'Second transaction',
      currency: 'NGN',
      source: '@account2',
      destination: '@account3',
    },
  ],
});
batch, _, _ := client.Transaction.CreateBulk(blnkgo.CreateBulkTransactionRequest{
    Atomic: true,
    Inflight: true,
    Transactions: []blnkgo.CreateTransactionRequest{
        {
            PreciseAmount: big.NewInt(10000),
            Precision: 100,
            Reference: "tx_003",
            Description: "First transaction",
            Currency: "NGN",
            Source: "@account1",
            Destination: "@account2",
        },
        {
            PreciseAmount: big.NewInt(5000),
            Precision: 100,
            Reference: "tx_004",
            Description: "Second transaction",
            Currency: "NGN",
            Source: "@account2",
            Destination: "@account3",
        },
    },
})
response = blnk.transactions.create_bulk({
  "atomic": True,
  "inflight": True,
  "transactions": [
    {
      "precise_amount": 10000,
      "precision": 100,
      "reference": "tx_003",
      "description": "First transaction",
      "currency": "NGN",
      "source": "@account1",
      "destination": "@account2",
    },
    {
      "precise_amount": 5000,
      "precision": 100,
      "reference": "tx_004",
      "description": "Second transaction",
      "currency": "NGN",
      "source": "@account2",
      "destination": "@account3",
    },
  ],
})
ApiResponse<JsonNode> response = blnk.transactions().createBulk(
    BulkTransactions.create()
        .atomic(true)
        .inflight(true)
        .transactions(List.of(
            CreateTransactions.create()
            .preciseAmount(10000)
            .precision(100)
            .reference("tx_003")
            .description("First transaction")
            .currency("NGN")
            .source("@account1")
            .destination("@account2"),
            CreateTransactions.create()
            .preciseAmount(5000)
            .precision(100)
            .reference("tx_004")
            .description("Second transaction")
            .currency("NGN")
            .source("@account2")
            .destination("@account3")
        )));
{
  "batch_id": "bulk_c62f200b-905f-4983-a349-cadd279234aa",
  "status": "inflight",
  "transaction_count": 2
}

Commit the batch

Use batch_id to commit every hold in the batch. See Commit a split or a bulk batch.

Error handling

Bulk endpoints return structured error_detail objects for validation failures. For duplicate references and batch rollbacks, behaviour depends on skip_queue. See API error codes for the full catalogue.

Request validation errors

Blnk returns 400 when the request body fails validation before processing starts.

CodeWhen it happens
TXN_BULK_EMPTYtransactions array is empty
TXN_BULK_LIMIT_EXCEEDEDMore than 10,000 items in transactions
TXN_VALIDATION_ERRORA transaction object is null or invalid

For TXN_VALIDATION_ERROR, error_detail.details.index identifies the zero-based position in transactions[]:

{
  "error_detail": {
    "code": "TXN_VALIDATION_ERROR",
    "message": "transactions[1]: currency: cannot be blank; destination: either destination or destinations is required, not both.",
    "details": {
      "index": 1
    }
  },
  "errors": "transactions[1]: currency: cannot be blank; destination: either destination or destinations is required, not both."
}

Duplicate references

Duplicate-reference handling depends on whether items go through the queue.

On the default queued path, duplicate references are deduplicated in the queue and never recorded. The batch still returns 201 with status: "applied" representing the request was successfully submitted:

{
  "batch_id": "bulk_847544fb-2616-43be-a188-f6b8c8f20cf2",
  "status": "applied",
  "transaction_count": 2
}

When skip_queue: true, Blnk validates references inline. A duplicate fails the batch and returns 409 with TXN_DUPLICATE_REFERENCE.

For atomic batches, previously applied items in the same request are refunded via _refund child transactions.

{
  "batch_id": "bulk_305873c0-518a-470c-bf3a-bc3f3e0b562c",
  "error": "failed to queue transaction 2 (Reference: sqdup-17372-existing, Source: bln_b4819533-9570-4770-a1a0-25c191da0557, Destination: bln_34111155-4015-4e84-bf7c-13a60794bf0b, Amount: 4.00): transaction validation failed: reference sqdup-17372-existing has already been used. All transactions in this batch have been refunded.",
  "error_detail": {
    "code": "TXN_DUPLICATE_REFERENCE",
    "message": "failed to queue transaction 2 (Reference: sqdup-17372-existing, Source: bln_b4819533-9570-4770-a1a0-25c191da0557, Destination: bln_34111155-4015-4e84-bf7c-13a60794bf0b, Amount: 4.00): transaction validation failed: reference sqdup-17372-existing has already been used. All transactions in this batch have been refunded."
  }
}

Important notes

  1. When atomic is true, transactions are processed in the order provided in the request.
  2. Each transaction’s reference should be unique. On the default queued path, duplicates are silently deduplicated rather than rejected - see Duplicate references.
  3. When run_async is true, processing happens in the background and you’ll receive an immediate response with a batch ID.
  4. For large transaction batches, using run_async: true is recommended to avoid timeout issues.
  5. Webhook notifications for async processing contain the same detailed error information as synchronous responses, including rollback status.
  6. On the default queued path, status: "applied" means the bulk request was accepted. Filter on parent_transaction or meta_data.QUEUED_PARENT_TRANSACTION to verify final child statuses.