Skip to main content

Exporting data for analytics and BI

If your goal is to get Bezala data into a data warehouse, BI tool, or accounting analytics dashboard — Power BI, Tableau, Looker, Snowflake, BigQuery, or anything else — this article is the one to read first.

J
Written by Julia Winberg

Bezala has two distinct surfaces for data extraction, and they're optimised for different things. Picking the right one matters.

The two surfaces

Operational endpoints — the ones you've seen elsewhere in these docs: GET /api/transactions, GET /api/daily_allowances, GET /api/trips, GET /api/rewards. They're designed for live UIs and operational integrations. The records they return are detailed enough for accounting flows but lean for analytics — you'd have to make extra calls per record to expand cost centers, attachments, vendors, and so on.

Export endpoints — GET /api/export/receipts, GET /api/export/trips, GET /api/export/daily_allowances, GET /api/export/rewards. These are purpose-built for analytics. Each response includes:

  • A flat, denormalised record per expense — cost center names and external IDs already joined, voucher account codes, batch reference number, attachment URLs, all in one place.

  • Aggregated totals (total_amount, total_records) at the top of the response.

  • Richer filtering (asset account, expense account, country code, cost center) that's not available on the operational endpoints.

For analytics work, start with the export endpoints. Fall back to the operational ones only if you need a field they don't carry.

What an export response looks like

curl -H "Authorization: Bearer <token>" \   "https://app.bezala.com/api/export/receipts?date_range=01%2F03%2F2026%20-%2031%2F03%2F2026&state=accounted&per_page=500"

Notice the joins are pre-done: cost center names and IDs and external IDs in a single field, account codes alongside account IDs, the batch reference number that ties the row back to a batch document. This is the row you want sitting in your warehouse.

The full set of export endpoints

Endpoint

What it returns

GET /api/export/receipts

Receipts (transactions).

GET /api/export/trips

Mileage trips.

GET /api/export/daily_allowances

Per diems.

GET /api/export/rewards

Rewards (with total_tax_amount aggregate alongside total_amount).

GET /api/absences/export

Absences as an XLSX file (binary response).

GET /api/time_entries/export

Project time entries as XLSX.

GET /api/work_time_entries/export

Work time entries as XLSX, where exposed.

GET /api/expense_summary/export

A PDF summary of expenses matching a tag.

GET /api/batch_documents/export

A PDF summary of a filtered set of batches.

The first four (receipts, trips, daily allowances, rewards) all return the same shape and accept the same filters.

Filters on the export endpoints

Param

Use it for

state

Filter by lifecycle state (typically accounted for analytics).

date_range

Natural date of the expense. Format "DD/MM/YYYY - DD/MM/YYYY".

updated_at_range

The date the record was last edited. Useful for incremental loads.

user_id

Limit to one user.

cost_center_id

Limit to one cost center.

country_code (receipts only)

ISO 3166-1, e.g. FI.

asset_account_id (receipts only)

Limit to receipts paid via one asset account.

expense_account_id (receipts only)

Limit to one expense account.

q

Free-text search on description and amount.

extended

Pass true to include conditional values. Most analytics integrations don't need this.

sort / order

Sort key and direction. Default: most recently updated first.

per_page accepts up to several hundred records — paginate as on any list endpoint.

Field semantics

A few details about the export rows are worth understanding before you build a warehouse on top of them. These are the questions that come up most often.

One row per VAT line, joined by id

When a receipt has multiple VAT lines — a hotel bill split between "Accommodation" and "Meals" — the export returns one row per VAT line, not one per receipt. Every row from the same receipt shares the same id, so you group when needed:

This is the right shape for analytics — you slice spend by expense account, not by receipt — but a naive "count of receipts" needs COUNT(DISTINCT id), not COUNT(*).

credit_acc_code is always the payment method's account

credit_acc_code is always the accounting code of the asset account the receipt was paid from — company card, personal-card-reimbursable account, bank account. It does not change based on whether the row is a purchase or a refund.

debit_acc_code is the expense account for that VAT line — Meals, Travel, Software. On a refund or credit note, the convention stays the same: codes unchanged, amount is negative. The bookkeeping system reads the sign and produces the appropriate debit/credit entry.

Reviewer please verify the negative-amount convention. If Bezala's actual implementation swaps the codes on credit notes, or uses positive amounts with a type=refund flag, this section needs correction.

Trips and daily allowances don't carry account codes

Receipts carry credit_acc_code and debit_acc_code because the accounting treatment is per-line and the user picks. Trips and daily allowances don't — the asset and expense accounts for these are configured at the company level.

Reviewer please verify that the trips and daily-allowance export endpoints genuinely don't carry these fields. If they do (perhaps under different field names, or only for some company configurations), update this section.

If you need the accounting code on trips and daily allowances, the workaround is a join: read the company's trip / daily-allowance settings via GET /api/company once, and apply the same code to every row. The codes don't vary by record.

Negative amounts and refunds

Refunds, credit notes, and corrections show up as receipts with negative total_cost. Same lifecycle, same accounting batch. There's no separate "credit note" object type.

For analytics:

  • SUM(total_cost) GROUP BY user_id gives net spend (purchases minus refunds).

  • SUM(CASE WHEN total_cost > 0 THEN total_cost ELSE 0 END) gives gross spend.

  • COUNT(*) WHERE total_cost < 0 gives the count of refund lines.

If your dashboard hides refunds, do it explicitly. Don't assume the export only includes positive amounts.

Two patterns: full load vs incremental

Full load

Useful for the first warehouse build and monthly reconciliation runs.

curl -H "Authorization: Bearer <token>" \   "https://app.bezala.com/api/export/receipts?state=accounted&date_range=01%2F03%2F2026%20-%2031%2F03%2F2026&per_page=500&page=1"

Page until collection.length < per_page. The total_records field tells you how many to expect.

Incremental load

Filter on updated_at_range. For analytics specifically, combine with state=accounted — only pull rows once they're locked, so your warehouse never has to handle "this row used to be approved but now it's been disapproved."

Wiring exports into specific BI tools

Power BI. Web data source, paging function per resource, hourly refresh. Tableau. Web Data Connector, or land in SQL warehouse first. Looker. Land in warehouse first; LookML is built for SQL. Custom Python/Node ETL. Most common path. Loop with per_page=500, write to warehouse with external_id as natural key.

Don't go directly from the BI tool to Bezala for production dashboards. Land in a warehouse first.

Pulling a closed batch in one shot

Returns every receipt, daily allowance, trip, and reward in that batch, grouped by type. See SDI-274 / article 09 for the full pattern.

Attachments

Attachment URLs in export rows are short-lived signed S3 links — they work for a few minutes. Don't store the URL itself in your warehouse and try to use it tomorrow; it will have expired.

Instead, when you ingest a row, fetch the attachment, store it in your own object storage, keep a reference to your copy.

If you need a long-lived link, use GET /api/attachments/:id/image — returns the image content directly against your authenticated session.

A practical analytics warehouse schema

Table

Source endpoint

Primary key

Natural key for upsert

bezala_receipts

GET /api/export/receipts

id

id (also store external_id if set)

bezala_trips

GET /api/export/trips

id

id

bezala_daily_allowances

GET /api/export/daily_allowances

id

id

bezala_rewards

GET /api/export/rewards

id

id

bezala_batches

GET /api/batch_documents

id

reference_number

bezala_users

GET /api/users/extended_index

id

email

bezala_accounts

GET /api/accounts

id

code

bezala_cost_centers

GET /api/cost_centers/extended_index

id

external_id if set

bezala_vendors

GET /api/vendors

id

external_id if set

Refresh: expense tables hourly, master-data tables nightly. This shape lets you build the standard finance dashboards — spend by department, spend by employee, top vendors, monthly close metrics — with simple SQL joins.

Did this answer your question?