Skip to main content

Sending expenses to accounting

Once an expense reaches the queue state, it's waiting for the accounting batch — the last step in Bezala's lifecycle. This article explains what a batch is, how it gets triggered, and what comes out the other side.

J
Written by Julia Winberg

What a batch is

A batch is a bundle of approved expenses that get sent to bookkeeping together. Each batch produces:

  • A bookkeeping voucher in the customer's accounting system (Netvisor, Procountor, Fortnox, NetSuite, Visma Nova, etc.). The voucher contains the journal entries — debits to expense accounts, credits to asset accounts — for every expense in the batch.

  • A SEPA payment file for any expenses that need to be paid out. Reimbursements to employees and vendor invoices both go here.

  • A PDF summary of the batch, signed with a structured reference number.

  • Updated state on every expense that was included: from queue to accounted. Locked from edits.

You access these through the batch documents endpoint family, not the individual expenses.

Two ways a batch fires

1. Scheduled

Every company configures its own batch schedule — daily, weekly, monthly, or one of the more granular options. The schedule lives on the company record:

In the response, look at:

  • schedule — the human-readable cadence ("daily", "weekly", etc.).

  • time_to_scheduled_batch_run.total — seconds until the next run.

  • format_time_to_scheduled_batch_run — the same thing as a human-readable string, e.g. "2 days, 4 hours".

When the scheduled time arrives, every expense currently in queue gets included.

2. Manual

An accountant or manager can trigger a batch right now:

Useful at month-end, when payroll is asking for the cut-off, or when you've just finished pushing a big block of expenses through the API and want them landed before the regular schedule fires.

The call returns immediately; the actual batch processing takes a few minutes. The expenses won't be in accounted state right away. Poll GET /api/expenses (where the _in_queue arrays should empty) and GET /api/batch_documents (where the new document will appear) to see when it's done.

Reading batches back

GET /api/batch_documents returns every successful batch document this user can see:

The fields worth knowing:

  • reference_number — Bezala's reference for the batch. Used as the voucher/payment reference downstream.

  • pdf_file_url — direct link to the human-readable PDF summary. The Bezala UI surfaces this for accountants.

  • accounting_integration_documents — links and metadata for the bookkeeping voucher in the connected accounting system.

  • other_integration_documents — links to ancillary outputs, most commonly the SEPA payment XML.

  • external_id — a string you can set yourself, typically the voucher number assigned by the bookkeeping system. See below.

The companion list endpoint, GET /api/expenses, returns the non-accounted expenses (everything before queue). To list expenses that have already been batched, use GET /api/archive, which paginates by batch date.

Pulling a batch's underlying transactions

GET /api/batch_documents gives you the metadata of a batch — its reference number, its PDF summary URL, its links to the integration documents in the connected accounting system. What it does not give you is the list of receipts, daily allowances, trips, and rewards that the batch contains.

To pull those, use GET /api/archive with the batch_reference_number filter:

The response groups the underlying expenses by type:

Each entry carries the full accounted-state record — the same shape you'd get from GET /api/transactions/:id and friends, plus a few additional fields (notably paid_at, batch_sending_failed, and the resolved cost-center metadata). This is the right call for the integration question "I have a batch reference; give me everything it contains".

Listing recent batches and walking each one

The full pattern for an integration that wants to mirror every closed batch into its own system:

In a real integration: cache reference_numbers you've already processed (use the external_id you set on the batch document via set_external_id), and skip them on subsequent runs. New batches show up at the top of the batch_documents list; once you've seen one, you've seen it.

Without a specific batch — by date

If you don't have a specific reference number, GET /api/archive paginates by batch date: each page returns one batch's worth of expenses, starting from the most recent batch and working backward. The has_more_items flag tells you when you've reached the end.

The list of available batch dates is also returned on GET /api/home, in the batch_dates field. That's useful for showing a "select a batch to view" dropdown without paging through archive.

When you want flat rows, not nested objects

GET /api/archive returns nested, type-grouped data — good for an operational view, less good for analytics. If you want flat, denormalised rows (one expense = one row, with cost-center names and account codes already joined), the /api/export/* endpoints give you that shape. They don't accept a batch_reference_number filter directly, but each row in their response carries batch_reference_number as a field, so you can filter client-side or drive the call with date_range matching the batch's date. See exporting data for analytics and BI.

Tying batches back to your bookkeeping system

If your integration owns the bookkeeping side, you'll typically want to write the voucher number Bezala generated into your system, and write your system's voucher number back into Bezala for traceability. The second direction is what external_id is for:

After this, anyone reading the batch document knows which voucher in the bookkeeping system it corresponds to. This is the sturdiest way to audit-trail across systems.

Filtered exports

If your finance team wants a PDF summary scoped to a specific user or date range, that's a two-step dance. First filter the batch documents:

Then fetch the export, passing the cookies from the first call so the server applies the same filter:

The second call returns Content-Type: application/pdf. The cookies are how Bezala remembers the filter between the two calls — it's a small quirk of the export design.

What can go wrong, and how to recover

Most batch failures fall into two buckets.

The accounting system rejected the voucher. Often a missing account in the customer's chart of accounts on the bookkeeping side, or a permission issue with the integration credentials. Bezala does not move the expenses to accounted if this happens — they stay in queue. You'll see the error on the batch attempt, and you can retry once the underlying issue is fixed.

The SEPA file generation failed. Usually a missing IBAN on a payee, or a structured-address requirement (relevant for SEPA changes from late 2026 onwards). The bookkeeping voucher will have gone through anyway; you can fix the IBAN and resend the SEPA file separately. The batch document carries a show_resend_sepa_link flag indicating when this option is available.

In both cases, the right place to act is the Bezala UI — the API surface is read-mostly for batches. For automated retries, monitor GET /api/expenses for expenses that linger in queue longer than expected and flag them for human attention.

Don't bypass the lifecycle

A common mistake when integrating is to imagine the batch is "just a sync" and try to bypass it — for example, by posting receipts and immediately marking them accounted because they were already processed elsewhere.

Don't. The batch is the contract between Bezala and the connected accounting system; it's also where SEPA payments come from. Bypassing it means SEPA never gets generated, the bookkeeping voucher is missing, and reconciliation downstream is broken.

If you have a use case where you genuinely don't want Bezala to send anything to accounting — e.g. you're using Bezala only as an expense-capture front end and you do bookkeeping yourself elsewhere — talk to support. There are configuration options at the company level that can disable specific outputs without bypassing the lifecycle.

Did this answer your question?