maesn
For developers

How to integrate with bexio: No read-only scope for the ledger

Bexio is quick to connect and slow to reason about. Two things decide what your integration can see, and neither of them is in your code: the scopes your app requests and the permissions of the person who clicked connect. For the ledger there is no narrow request to make, because every read-only scope bexio publishes names something other than accounting data.

Lennart Svensson, CTO and Co-Founder at Maesn
Lennart Svensson
CTO and Co-Founder · · Updated
Illustration for How to Integrate with bexio: The Ledger Has No Read Scope
The context

One connection, two sets of permissions

Getting a token is the ordinary part. Bexio runs OAuth 2.0 with the OpenID Connect extension, the issuer is auth.bexio.com/realms/bexio, and you register an app on its developer portal to get a client id and a client secret. Nothing there will surprise anyone who has done this before.

What decides your integration is the sentence after that. Bexio checks permission on two levels, and it publishes the fact instead of leaving you to find it: “The API access happens with the user rights of the user who set up the connection to the application.” So your app can hold a scope it cannot use, because the person who granted it cannot see the resource either.

Bexio's own authorisation matrix: the connecting user's right down the side, your client's scope across the top
User rightWith scopeNo scope
Has the rightAccess grantedAccess denied
Lacks the rightAccess deniedAccess denied

Both levels and the four outcomes from bexio’s authorization code flow section, checked 13 August 2026. Both have to hold at once, and a call is refused if either one is short, which is cheaper to encode before the error handling than after it.

The two levels have two status codes, and the reference separates them cleanly. A 401 means the bearer token is invalid. A 403 is the other problem, in bexio’s words “You do not possess the required rights to access this resource”. If you treat both as an authentication failure you will send customers back through a consent flow that was never the issue.

Which objects a connection reaches, field by field, is on the bexio API page. What the rest of this piece is about is the part that is not a list: the shape of the ledger behind it, and the two places permission is decided.

The request frame, and how scopes are writtenHTTP
GET /2.0/contact HTTP/1.1
Host: api.bexio.com
Accept: application/json
Authorization: Bearer {access-token}
 
# Multiple scopes are separated by whitespace, not commas:
# kb_offer_edit kb_invoice_edit

Both the call and the scope form are bexio’s own. Requests with a payload also need Content-Length, and Accept-Language decides whether some values come back readable at all, which the tax rates make expensive to discover late.

Two token lifetimes, sixty days and one year

A personal access token is the fast way to a first response and a bad way to run in production. It carries every default scope, which bexio describes as “full access to your company’s data”, it expires 60 days after creation, and revoking it can take “up to 1 hour” to take effect.

The real connection has a quieter deadline. With offline_access the refresh token is valid indefinitely, but the session behind it closes if it is not renewed, so in bexio’s words “tokens must be refreshed within 1 year”. A customer who goes quiet for thirteen months is a customer you have to re-onboard.

Token scope, validity and revocation delay from bexio’s personal access token section, the status codes from its header and response code list, the offline session from its identity provider notes, all checked 13 August 2026.

The problem

No read-only scope covers the ledger

Bexio publishes 35 scopes for its interface. Eighteen of them grant read access to one resource, and not one of the eighteen names accounting data. The only scope that does name it reads, in full, “Write access to accounting data”.

Most resources come as a pair, contact_show next to contact_edit. Two scopes have no suffix at all. One of them, file, says in as many words that it covers “Read and write access”. The other is accounting, and it has no reading half.

The 35 scopes bexio publishesCounted in the table
18
End in _show

Read access to one resource

15
End in _edit

Write access, and read comes with it

2
Neither suffix

file and accounting

The six objects you can read, and the narrowest scope that covers each
  • Contactscontact_show
  • Itemsarticle_show
  • Bank accountsbank_account_show
  • AccountsNo read-only scope
  • Journal entriesNo read-only scope
  • Tax ratesNo read-only scope

The three without a narrow option are the ledger. Accounts, journal entries and tax rates are the objects an accounting integration exists for, and the only scope that names their data describes itself as write access.

Counted over bexio's published scope table on 13 August 2026: 35 scopes, 18 read-only, and three of the six objects we can read have no read-only scope naming them.

Scope names, descriptions and the whitespace rule from bexio’s API scopes table, checked 13 August 2026, counted line by line. Five OpenID Connect scopes sit in a second table and are not part of this count.

There is a rule that makes this workable and also makes it visible. Bexio grants reading with writing: “Read access is granted automatically when a write scope is requested”. Ask for contact_edit and you do not need contact_show. Applied to the ledger, it means the documented way to read a booking is to ask for permission to write one.

That lands somewhere your architecture diagram does not reach. Bexio shows the user “a consent page listing the permissions” your app is asking for, and for a product that only reads the books, the narrowest available request still reads as write access. That belongs in the conversation with the customer, and it is an easier one to have before the first connection than after it.

What this does not tell you

Bexio documents the scope table and the automatic read grant. It does not print a required scope on the individual account, journal or tax endpoints, so which narrower combination might work in practice is untested here rather than ruled out. What the table does support is narrower: there is no documented read-only ask for accounting data.

The problem

Reading a booking and writing one are different resources

A bexio booking has two addresses. You read the ledger under GET /3.0/accounting/journal, which the reference files under Reports. You write to it under POST /3.0/accounting/manual_entries, filed under Manual Entries. They are not two verbs on one resource.

The shapes differ with them. A row you read carries debit_account_id and credit_account_id together, plus a ref_class such as KbInvoice that tells you which document produced it. A row you write belongs to an entry with a type, and the type decides how many accounts each line may touch.

One bexio ledgerTwo resources
Reading a bookingReports
GET /3.0/accounting/journal

One row carries debit_account_id and credit_account_id together.

Documented filters
from · to · account_uuid
Writing a bookingManual Entries
POST /3.0/accounting/manual_entries

One of three shapes: single, compound or group entry.

Required fields
type · date · entries

Both sides arrive as one object. A journal entry carries line items, and each line says whether it is a debit or a credit, so bexio’s one row with two accounts becomes two lines and the compound case stops being a special path.

Endpoints, filters and required fields as bexio's reference lists them, checked 13 August 2026.

Bexio documents three entry types and gives each one a worked example. A manual_single_entry is one debit against one credit. A manual_compound_entry takes one side and distributes the amount across several accounts. A manual_group_entry posts several one-line bookings that share a reference_nr, which is capped at 80 characters.

A single entry, in the documented shapeJSON
{
"type": "manual_single_entry",
"date": "2026-08-13",
"reference_nr": "Booking BA-22",
"entries": [
{
"debit_account_id": 77,
"credit_account_id": 139,
"tax_id": 3,
"tax_account_id": 77,
"description": "Payment for client Smith",
"amount": 328.25,
"currency_id": 1
}
]
}

Tax arrives as two fields. tax_id is the rate and tax_account_id is where the tax posts, and the pairing is what the validation checks.

Entry types, required fields, the 80 character limit and the payload shape from bexio’s create manual entry endpoint, the journal fields from its journal endpoint, both checked 13 August 2026.

A successful write can come back locked

The response to a created entry carries is_locked and, next to it, locked_info with the documented value closed_business_year. A booking can therefore be accepted and immediately be beyond editing, which is correct accounting behaviour and a surprise if your code assumes that a 201 means the record is still yours to change.

One more asymmetry decides what you cache. The journal can be filtered by account_uuid, and the rows it returns identify accounts by debit_account_id and credit_account_id, which are integers. GET /2.0/accounts gives you both an id and a uuid, so you need to hold the pair: one form to ask with, another to read back.

Two resources and two identifier forms for one concept is exactly the seam a normalised model is for. What a booking looks like once it arrives in one shape, here and on the systems next to it, is the common data model.

How it works

Three of the six readable objects cannot be written at all

Six objects are readable on bexio through our connection, and the interesting number is not six. Three of them have no write endpoint at bexio in the first place: the chart of accounts, the bank accounts and the tax rates are things you look up, not things you create.

That is measurable twice. Our own coverage data marks those three as unsupported by bexio for create and update, and bexio’s endpoint inventory agrees: the Accounts resource documents a list call and a search call, and nothing else.

What bexio's own resource reference exposes per object, checked at the vendor
ObjectAt bexio
ContactsFull resource
Journal entriesYes, via manual entries
ItemsFull resource
AccountsList and search only, no write endpoint
Bank accountsNo write endpoint exists
Tax ratesDelete only, no create or change

Endpoint inventory from bexio’s accounts and taxes resources, checked 13 August 2026. Tax rates are the odd row: bexio publishes a delete call and no way to create or change one.

bexio does not offer credit notes

Our own live article says corrections require credit notes. Bexio’s own FAQ says otherwise: “Are Credit Notes available? No, currently Credit Notes are not available but we have plans to put it online.” The immutability is real, and the remedy the live article names is not something bexio exposes. Corrections are planned as new documents on this system.

The chart of accounts is where the read-only surface gets interesting, because the field that classifies an account is a number. A bexio account returns account_type as an integer, and the value set is nowhere in the reference: across 34.955 lines it appears three times, twice as the value 1 in a response sample and once as a searchable field name.

Two layers sit on top of each other here, and they are different facts. Through our endpoint an account carries a readable class, and its documented values are ASSET, EXPENSE, LIABILITY, REVENUE and COMPLETE. There is no EQUITY in that list, and COMPLETE is not a balance sheet class, so a mapping over an undocumented integer is exactly what it looks like.

The hierarchy takes a second call. An account points at a group with fibu_account_group_id, groups point at their parent with parent_fibu_account_group_id, and GET /2.0/account_groups is where they live. Where the raw values matter, rawData is a documented query parameter on our side and returns the untouched payload.

Writing has its own price, and it is paid in lookups. Creating a contact needs four required fields, and two of them are people: contact_type_id (1 for companies, 2 for persons), name_1, user_id and owner_id. Salutation, title, country and language are all id references to their own resources, which is seven lookup tables standing behind one contact record.

Account payload, group fields and the required contact fields from bexio’s create contact endpoint and its accounts resource, checked 13 August 2026. The class values and rawData are from our own endpoint reference for bexio, read the same day. The three occurrences of account_type were counted in the full reference text.

A deprecation that only runs one way

Since 9 December 2025 the address field is deprecated in the request payloads of the contact and address endpoints, and bexio notes it is “still present in responses for backward compatibility”. You have to stop sending it and keep parsing it, which is the kind of change a schema test written on responses alone will not notice.

How it works

A tax rate is valid for a period, and its name is a translation key

A bexio tax rate is not a constant with a percentage on it. It has a start and an end, in both years and months, and the endpoint gives you a filter that exists for exactly that reason: date, documented as “Displays all taxes which are active at the date given”.

Bexio’s own sample row makes the point better than an argument would. It carries a rate of 7.7 with start_year 2017 and end_year2018, so the correct rate for a document is a function of the document’s date and not of today. Two rows can share a code and differ in percentage.

Asking for the rates that were active on a dateHTTP
GET /3.0/taxes?date=2026-08-13&types=sales_tax&scope=active
Host: api.bexio.com
Accept: application/json
Accept-Language: de
 
# One row of the documented response, abbreviated:
# { "code": "UN77", "digit": "302", "value": 7.7,
# "name": "lib.model.tax.ch.sales_7_7.name",
# "account_id": 98, "start_year": 2017, "end_year": 2018 }

types takes sales_tax or pre_tax, scope takes active or inactive, and digit is the figure the Swiss return expects.

Look at name in that row. The value is a translation key, and it arrives that way unless you ask for a language. That is what the Accept-Language header in the header section is for, and bexio says so directly: it is “useful to have the tax codes in the user’s language”. Put that key in a dropdown once and a customer will send you a screenshot.

The rate also knows where it posts. A tax carries an account_id and an account carries a tax_id, so the two point at each other, and a write that pairs them wrongly comes back 422 instead of being quietly accepted. That is the real mechanic behind the reputation Swiss tax handling has here.

One limit of the abstraction sits here. The tax rate you get through our endpoint has five fields, id, code, description, name and percentage. None of them is a validity window. Where a back-dated document needs the rate that applied then, that answer is in the raw payload.

Query parameters, the response fields and the quoted header guidance from bexio’s tax list endpoint and its header section, checked 13 August 2026. The five unified fields come from our own bexio field list for tax rates, read the same day. Nothing here rests on a rate change outside bexio’s own documentation.

Where the reference is quieter than its reputation

Swiss reverse charge, Bezugssteuer, is usually the first thing named about bexio integrations, including in our own live article. Neither term occurs anywhere in bexio ’s reference: zero hits across 34.955 lines. What is documented is what this section uses, the pairing of rate and account and the validity window. The rest is a conversation with the customer’s accountant.

How it works

Nothing pushes, and the only date filter is the booking date

No object on bexio carries a native event in our coverage data, so nothing calls you. Whatever you learn about a customer’s books, you learn by asking. That much bexio shares with several systems in the catalogue.

The second half is specific to bexio, and it is what a design has to absorb. The journal takes from and to, and those bound the booking date. A booking date is not the moment a row appeared. Post a back-dated entry today and it lands behind any window drawn on that date.

What you can ask the journal, on bexio and through us
WhereFiltersPage size
bexio, directlyfrom, to, account_uuidup to 2000
Through our endpointpage onlyup to 100
Business Central, for contrastlastModifiedAt, filterup to 100

The native filters and the 2000 ceiling from bexio’s journal endpoint; the two right-hand rows from our own field lists for the same unified endpoint, where the bexio panel offers limit, page and rawData while the Business Central panel next to it offers lastModifiedAt, described as “Filter by last modified date”. All checked 13 August 2026.

The middle row of that table has a companion detail. Through our endpoint, transactionDate is a response field on a bexio journal entry: it comes back on every row, and it is absent from the query parameters. The date is available after you have read the data, which is the wrong order for a delta.

So a sync on bexio has two moving parts and no shortcut. You choose a cadence, and you choose an overlap wide enough to catch entries posted late into an earlier period. What that costs in calls is the trade, and the platform does not make that decision for you.

What one event model over systems that have events and systems that do not actually delivers, including where it stops, is the unified webhooks page.

A ceiling you can only read at runtime

Bexio does enforce a rate limit and does not publish the number. Its reference says the limit “limits the number of requests a company can make per minute”, with 429 above it and three headers to read: RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset.

Two consequences follow. The budget is per company, so it is shared with everything else that customer has connected, and your own throughput is not the whole picture. And a limiter you can only calibrate from response headers has to be built adaptive, because there is no value to configure.

Bexio is not alone in this: our asynchronous processing page lists the systems that enforce a limit without publishing its value, and the ones that publish four different units for the same idea.

Rate limit wording, status code and header names from bexio’s rate limiting section, checked 13 August 2026.

One further gap changes how you start. Bexio publishes no machine-readable schema: “No, we currently do not provide an OpenAPI definition but we have plans to put it online.” There is no client to generate, so the model you write by hand is the model you maintain.

How paging and filtering read once they are the same across systems, cursors and offsets alike, is the unified pagination and filtering page.

What you get

Building needs no permission, the listing costs a quarter

The commercial half is unusually clear, and it splits at a line bexio draws itself: “We do not restrict API access: anyone can develop an integration with bexio.” Nothing is gated before you build, and nothing is gated before you go live. The marketplace is the part with conditions.

Eight of them, published as a list. At least 10 bexio customers among your regular clients and at least 20 active users of your app, a defined target group and a user-friendly integration.

The other four are easy to underestimate. Language coverage spans German, French, Italian and English, with the app in two of them and the listing content in all four. Then integration through the interface plus OpenID Connect login, your own customer support and a signed partner agreement.

All eight requirements and the quotation from bexio’s become a marketplace partner page, checked 13 August 2026. The same page gives the context for the numbers: bexio reports over 100.000 customers and more than 1.000 certified fiduciary partners, and says the marketplace sees 10.000 visitors a month.

The number our own article left vague

There are no annual fees, no membership dues and no listing costs. Instead bexio charges “a service fee of 25% on the revenue generated with shared bexio users”, where a shared user is a customer who uses both products. Rank in the “top 20% revenue-generating partners” and the fee falls to 20 per cent. There is no alternative: bexio states it does not offer other billing models.

Two clauses travel with the fee and belong in the same conversation. Cession means bexio collects the payments for your solution from bexio customers directly. And price parity obliges you to offer the same conditions on your other channels, which touches pricing and not only paperwork.

One requirement in that list is routinely misread, our own live article included, so it is so it goes here. OpenID Connect is a login requirement and not a second authentication layer on the interface. Bexio’s interface authentication already “implement[s] the OAuth2 protocol with the OpenID connect extension”.

What the marketplace additionally asks for is OIDC as a login, in bexio’s own description a protocol that “allows users to log in to your solution using their existing bexio credentials”. One protocol, two purposes: authorising calls, and signing people in. The second is a feature of your product, not a change to your integration.

Getting an environment to test in costs nothing either way. Bexio offers a free trial account, and we provide a bexio sandbox for teams who would rather not manage one. Only one step in the whole setup is not self-service: the client id and secret reach us through your contact instead of a form.

Fee, reduction, cession, price parity and the OIDC description from bexio’s marketplace partner agreement page; the protocol wording from its own identity provider notes; the trial and sandbox from our sandbox table. All checked 13 August 2026. Bexio also flags the commercial use terms in section 4.4 of its marketplace conditions, and that section covers commercial use.

Where our part ends

What Maesn covers, and what stays with you

The connection itself is short work on bexio, which is exactly why the rest deserves the attention. What we take off the list:

  • Two resources become one object. The journal you read and the manual entry you post arrive and depart as the same journal entry, with line items that each say whether they are a debit or a credit, so the compound case stops being its own code path.
  • The id lookups collapse. Salutations, titles, countries, languages and the account groups behind a chart of accounts are resolved for you, so a reference synchronisation layer is not something you maintain to create one contact.
  • The two identifier forms stop leaking. Filtering by account_uuid and reading integer account ids back is handled behind one interface, along with the paging that differs from every neighbouring system.

Three things stay with you, and two of them are decisions before they are work:

  • Which permissions you ask for, and how you explain them. We do not make the ledger scope narrower than bexio publishes it, because that is not ours to change. What the consent screen says, and what you tell a customer it means, is yours.
  • The cadence and the overlap. Nothing turns bexio into an event source, because it is not one, and no filter turns a booking date into a change timestamp. How fresh your product needs to be, and how far back each pass reaches, we can inform and not replace.
  • The marketplace application and its price. The ten customers, the twenty users, the four languages and the 25 per cent are between you and bexio. Nothing in any interface changes that arithmetic.

One sentence on the part of the live article this piece leaves aside. Invoices in bexio do move through strict states, with issue, revert, cancel and mark-as-sent as separate calls, and once booked the core fields stop changing. Through our connection invoices are marked on demand instead of supported, so this piece argues the objects that carry weight today.

The invoice state calls are named in bexio’s invoices resource, checked 13 August 2026. The on-demand marking comes from our own coverage data, where it records what is technically reachable, not what is available now.

FAQ

Frequently asked questions

Where do I get a bexio key for my integration?

There is no key to fetch. Bexio issues OAuth clients: you register an app on its developer portal and receive a client id and a client secret. For personal use there is a Personal Access Token instead, which carries every default scope and expires 60 days after it is created.

Why does my bexio request return 403 when the scope was granted?

Because permission is checked twice. Bexio states that access happens with the rights of the user who set up the connection, so a granted scope still fails if that person cannot see the resource in the bexio interface. A 401 means the token is wrong, a 403 means the rights are.

Can I import a chart of accounts into bexio?

Not through the interface. The Accounts resource documents a list call and a search call and nothing that writes, so accounts are read-only for an integration. The same holds for bank accounts and tax rates: they exist to be read and referenced, not created.

How do I post a booking to bexio?

Through manual entries, which is a different resource from the journal you read. You choose one of three documented shapes: a single entry with one debit and one credit, a compound entry that splits one side across several accounts, or a group entry that shares a reference number.

Does bexio support webhooks?

No object in our coverage data carries a native bexio event, so nothing calls you. What makes the design harder than it sounds is the second half: the journal can be filtered by booking date, and a booking date is not the moment the row appeared, so a back-dated entry lands behind your window.

What are the bexio rate limits?

Bexio limits requests per company per minute and returns 429 above that, but it does not publish the number. Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset, so the ceiling is something you read at runtime rather than a value you can design against.

What does a bexio Marketplace listing cost?

There is no annual fee, no membership due and no listing cost. Bexio charges a service fee of 25 percent on revenue generated with shared users, reduced to 20 percent for the top 20 percent of revenue-generating partners. Building an integration needs no permission at all.

Build once on the Unified API.

Bexio splits its ledger across two resources and publishes no number for its rate limit. The next system will draw both lines somewhere else. Build against one interface and each of those differences turns into a field you read instead of a project you schedule.