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 outAccounting 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
| Status | Meaning | Exists in Brinta? |
|---|---|---|
succeeded | Created by this batch | Yes, new |
skipped | Already existed, nothing done | Yes, pre-existing |
failed | Rejected by validation, or failed to persist | No |
not_processed | The batch aborted before reaching it | No |
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_reason | Cause |
|---|---|
Incorrect Entry General Info | Invalid book_type or entry_category, malformed entry_date |
Incorrect Entry Lines | A line with neither debit nor credit, negative amounts, missing currency |
Unbalanced Entry | sum(debit) does not equal sum(credit) on a JOURNAL entry |
Account Not Found | account_external_id does not resolve |
Cost Center Not Found | center_external_id does not resolve |
Invoice Not Found | invoice_id does not resolve |
Company Not Found | Neither entry_company_id nor entry_company_external_id resolves |
Duplicate External Id | The 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
| Limit | Value |
|---|---|
| Entries per batch | 10,000 |
| Total lines per batch | 100,000 |
| Request body size | 10 MB |
Concurrent batches in processing, per company | 3 |
| Batch creation requests | 10 per minute |
| Entries | 200,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.
Updated about 17 hours ago
