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 | |
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.



