Sending accounting entries in batches

Submit up to 10,000 accounting entries in a single request, with per-entry results and safe retries.

Accounting entries arrive in volume by nature. A month-end close is thousands of journal entries, each with two or more lines, and calling POST /accounting-entries/ once per entry means paying a full round trip for what is essentially a bulk insert.

POST /accounting-entry-batches/ takes up to 10,000 entries in one request and tracks the whole load as a single object.

📘

Two ways in, one way out

Accounting entries have their own submission endpoint because their payload has nothing in common with a sale or a transfer. But once submitted, a batch is a batch: you read progress and results through the same GET /batches/{batch_id} endpoints used by transaction batches. One status mechanism, one results format, one error format.

Sending a batch

Each element of entries is exactly the body you would send to POST /accounting-entries/.

curl -X POST https://api.brinta.com/accounting-entry-batches/ \
  -H "Authorization: Bearer $BRINTA_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "options": { "on_duplicate": "skip" },
    "entries": [
      {
        "entry_external_id": "JRN-2026-08-000412",
        "entry_company_external_id": "CLI-99812",
        "entry_date": "2026-08-14T00:00:00Z",
        "book_type": "JOURNAL",
        "entry_category": "NORMAL",
        "currency": "ARS",
        "description": "Venta mostrador 0001-00001234",
        "lines": [
          { "line_external_id": "L1", "account_external_id": "1.1.01.001",
            "center_external_id": "CC-VENTAS",
            "debit": 12100, "credit": 0, "currency": "ARS" },
          { "line_external_id": "L2", "account_external_id": "4.1.01.001",
            "debit": 0, "credit": 12100, "currency": "ARS" }
        ]
      }
    ]
  }'

Two things are worth calling out, because they are what makes this endpoint straightforward compared to transaction batches.

There is no discriminator. A transaction batch requires a transaction_type and every entry has to match it. Accounting entries do not work that way: book_type and entry_category live on each entry, so a single batch can carry NORMAL journal entries alongside CLOSING ones. A month-end close mixes them legitimately, and forcing that into separate requests would be arbitrary.

Nothing has to be looked up first. entry_external_id is already required on the single-resource endpoint, so it serves as the correlation and deduplication key with no changes on your side. And the company can be referenced with entry_company_external_id, so you never need to resolve Brinta IDs before you can build the request.

The one exception is invoice_id, which is a Brinta identifier. If you are not already storing Brinta invoice IDs, omit it rather than making thousands of lookup calls to populate it.

Getting the result

The response is always 202 Accepted, no matter how many entries you send.

{
  "batch_id": "bat_01K2MB7F9QT",
  "kind": "accounting_entries",
  "status": "queued",
  "dry_run": false,
  "created_at": "2026-08-17T14:40:00Z",
  "summary": { "total": 812, "succeeded": 0, "failed": 0, "skipped": 0, "pending": 812 },
  "links": {
    "self": "https://api.brinta.com/batches/bat_01K2MB7F9QT",
    "results": "https://api.brinta.com/batches/bat_01K2MB7F9QT/results"
  }
}

Structural problems are still reported synchronously, before the batch is queued: an empty entries array, more than 10,000 entries or 100,000 total lines, a missing or duplicated entry_external_id, or a line_external_id repeated within the same entry all come back as a 422 immediately.

From here on, everything works exactly as it does for transaction batches:

# Long-poll until the batch finishes, up to 60 seconds
curl "https://api.brinta.com/batches/bat_01K2MB7F9QT?wait=30" \
  -H "Authorization: Bearer $BRINTA_TOKEN"

# Then read only what failed
curl "https://api.brinta.com/batches/bat_01K2MB7F9QT/results?status=failed" \
  -H "Authorization: Bearer $BRINTA_TOKEN"
{
  "batch_id": "bat_01K2MB7F9QT",
  "results": [
    {
      "index": 0,
      "external_id": "JRN-2026-08-000412",
      "status": "succeeded",
      "id": "ae_01K2MB8C4XW"
    },
    {
      "index": 7,
      "external_id": "JRN-2026-08-000419",
      "status": "failed",
      "http_status": 422,
      "errors": [
        {
          "error_reason": "Account Not Found",
          "error_detail": "No account matches external id 4.1.01.999 for this company.",
          "error_field": "lines[1].account_external_id"
        }
      ]
    }
  ],
  "pagination": { "next_cursor": null, "has_more": false }
}

external_id holds your entry_external_id, and id holds the accounting entry ID Brinta assigned. The result record is deliberately generic so that both kinds of batch come out of the same endpoint with the same shape; the batch's kind tells you which one you are looking at.

error_field paths are relative to the entry. lines[1].account_external_id is the second line of that entry.

The four outcomes

StatusMeaningExists in Brinta?
succeededCreated by this batchYes, new
skippedAlready existed, nothing doneYes, pre-existing
failedRejected by validation, or failed to persistNo
not_processedThe batch aborted before reaching itNo

skipped is what makes a retry safe. If your process dies halfway through a 5,000-entry close, resend the whole batch: the entries that already landed come back as skipped, the rest as succeeded, and nothing is duplicated. Set options.on_duplicate to error if you would rather be told explicitly.

Together with the Idempotency-Key header, which returns the original batch_id untouched if you replay the same request within 24 hours, you have two layers of protection covering the immediate retry and the late one.

Errors specific to accounting entries

Errors use the standard Brinta error envelope, extended with error_field. These reasons are specific to entries:

error_reasonCause
Incorrect Entry General InfoInvalid book_type or entry_category, malformed entry_date
Incorrect Entry LinesA line with neither debit nor credit, negative amounts, missing currency
Unbalanced Entrysum(debit) does not equal sum(credit) on a JOURNAL entry
Account Not Foundaccount_external_id does not resolve
Cost Center Not Foundcenter_external_id does not resolve
Invoice Not Foundinvoice_id does not resolve
Company Not FoundNeither entry_company_id nor entry_company_external_id resolves
Duplicate External IdThe entry already exists and on_duplicate is error

Validating before you commit

Set options.dry_run: true to run full validation without storing anything. Successful entries come back as succeeded with no id, and the batch echoes dry_run: true.

This is worth doing on the first load of a new chart of accounts. Account and cost center references are the most common source of failure, and finding out that 300 entries point at an account code that does not exist is much cheaper before the write than after.

Limits

LimitValue
Entries per batch10,000
Total lines per batch100,000
Request body size10 MB
Concurrent batches in processing, per company3
Batch creation requests10 per minute
Entries200,000 per hour, per company

The line cap usually binds before the entry cap. A double-entry journal has 2 lines, but a payroll entry can have 200, so 10,000 entries is not a meaningful ceiling on its own.

📘

Entries within a batch are processed in parallel and completion order is not guaranteed. Each entry is stored atomically with all of its lines, but the batch as a whole is not a single transaction.


Did this page help you?