Skip to main content

Conventions, pagination, and dates

The Bezala API is consistent enough that once you've used a couple of endpoints you can guess the rest. This article spells out the conventions explicitly so you don't have to.

J
Written by Julia Winberg

Base URL and content type

All endpoints live under:

https://app.bezala.com/api/

Send Content-Type: application/json on every POST and PUT. The API also accepts multipart/form-data for endpoints that take file uploads (notably POST /api/attachments and POST /api/bills/import); when you upload binary, the file part is binary and the rest of the body is form fields.

Responses are JSON unless explicitly noted. The export endpoints — GET /api/batch_documents/export, GET /api/absences/export, GET /api/time_entries/export, and the /api/export/... family — return PDF or XLSX binary content with the appropriate Content-Type header.

IDs

IDs are integers. They're not globally unique — a transaction with id: 42 and a vendor with id: 42 are unrelated objects. References between objects are typed (vendor_id, cost_center_id, user_id), and most objects also support an external_id field, which is a string of your choice that you can use to map Bezala records to records in your own system.

We strongly recommend setting external_id on every object you create through the API. It makes idempotent upserts straightforward and survives Bezala IDs changing if a record is recreated.

Pagination

List endpoints paginate by default. Two query parameters control it:

  • page — 1-indexed page number. Default: 1.

  • per_page — records per page. Default: 50. Some endpoints allow up to 1000.

A list response is always a JSON array; pagination metadata (total counts, last page) is not in the response body. To detect the end of a list, request a page and check whether you got fewer records than per_page — if so, you're done.

A few endpoints (notably bills, batch documents, and bill lines) accept skip_pagination=true to return everything in one shot. Use it for nightly exports where the convenience is worth the bigger response. Don't use it for interactive UIs.

Filtering and polling

Most list endpoints accept the same filters:

  • updated_after — a Unix timestamp. Returns only records updated on or after that moment. This is the right filter for a polling integration: store the timestamp of your last successful poll, send it next time, get only the deltas.

  • date_range — a string of the form "DD/MM/YYYY - DD/MM/YYYY", e.g. "01/01/2026 - 31/01/2026". Filters by the record's own date (the receipt date, the absence date, the bill date), not by when it was edited.

  • user_id — limit to a single user's records. Whether this works depends on the caller's role: regular users only see their own records anyway, so this filter is mostly relevant for managers and accountants.

  • state — limit to a specific lifecycle state. The valid values depend on the object type. See the expense lifecycle.

Dates and times

Bezala uses three date/time formats, depending on the field:

Format

Example

Where it shows up

ISO 8601 date

2026-12-31

All start_date, end_date, date, receipt date fields.

ISO 8601 datetime, UTC

2023-02-16T13:00:40.746Z

Read-only created_at and updated_at on every object.

DD/MM/YYYY - DD/MM/YYYY

01/01/2026 - 31/01/2026

Only the date_range filter on list endpoints.

Unix timestamp (seconds)

1740649659

The updated_after polling filter and the reported_at field on absences and time entries.

Times-of-day, where supported (for example, the start_time and end_time of a partial-day absence), are 24-hour strings in UTC, e.g. "13:00". Bezala does not store local times — convert to UTC before you send.

Currency amounts on most write endpoints are sent in cents (or the smallest denomination of the currency). On read, you'll see them either as cents or as decimal strings depending on the endpoint. When in doubt, check the example payloads in /apipie.

Hierarchical parameter names

Most write endpoints accept a top-level wrapper key whose value is a hash of fields, often with nested arrays of attributes. For example, creating a receipt:

The pattern xxx_attributes (with the _attributes suffix) is a Rails convention for nested writes. You can:

  • Create new nested records by including them with no id field.

  • Update existing nested records by including their id and the changed fields.

  • Delete nested records by including their id and _destroy: true.

Some endpoints document this explicitly (account approvers, budget lines, VAT lines); others use the same pattern implicitly. When in doubt, look at the request examples on the apipie reference page for that endpoint.

Polling, not webhooks

Bezala does not currently emit webhooks. To detect changes, poll list endpoints with updated_after.

A polling integration looks like this:

  1. Store the Unix timestamp of your last successful poll.

  2. On each cycle, call the list endpoint with updated_after=<that timestamp>.

  3. Process the response.

  4. Update the stored timestamp to the moment you started this cycle (not the moment you finished — that way records modified during processing are picked up next cycle).

For most use cases, polling every 5–15 minutes is plenty. If you need near-real-time, every 30 seconds is fine; we'll let you know if you're putting noticeable load on us.

Idempotency

The API does not currently enforce idempotency keys on POST requests. If a network failure causes you to retry a POST, you risk creating two of whatever you were creating.

Two safeguards:

  1. Use external_id. Most objects accept an external_id you set yourself. Before retrying a creation, check if a record with that external_id already exists. If yes, skip the create and treat the existing record as the result of your request.

  2. Time out generously. Bezala can be slow on heavy operations (a big receipt with OCR processing, a large bill import). Don't retry until you're sure the request is actually dead — typical timeout of 60 seconds is reasonable.

Charset and locale

The API is UTF-8 in and out. If you're sending Finnish, Swedish, Norwegian, or any other non-ASCII text, encode it as UTF-8 and you're fine. The user's interface locale (the locale field on the company object) does not affect API responses — field names, error messages, and field values come back in their canonical form regardless.

Did this answer your question?