Skip to main content

Authentication

Every Bezala API call must be authenticated. Authentication identifies a user, scopes the call to that user's active company, and limits what the call can do based on that user's role (regular user, manager, accountant).

J
Written by Julia Winberg

Bezala uses Bearer tokens. There are four ways to get one.

Get a token with email and password

The basic flow is email + password → token:

On success you get back the token, the user ID, and the user's personal receipt-forwarding email address:

On failure you get a 401:

Get a token via Google, Microsoft, or Okta

If your users sign in to Bezala through SSO, exchange the provider's access token for a Bezala token. The shape of the call is the same in each case — you provide the email Bezala expects and the access token issued by the provider:

Bezala validates the access token against the provider, confirms the claimed email matches what the provider says, and issues a Bezala token. From then on, the SSO token is irrelevant — you authenticate to Bezala with the Bezala token alone.

Use the token on every call

Send the token in an Authorization header:

There is also a legacy fallback — passing ?token=... as a query parameter — that works on most endpoints. Don't use it. Query parameters end up in server logs, browser histories, and analytics tools. The header form is the only safe choice.

Token lifecycle

Tokens are long-lived. They don't expire on a fixed schedule, but they can be invalidated by:

  • The user changing their password.

  • An admin removing the user from the company.

  • The user being deleted.

If you start getting 401 Unauthorized on calls that used to work, get a fresh token. Don't try to refresh on every call — that's a slow, unnecessary round-trip.

Multi-company users

A single user can belong to several companies. The token doesn't encode which company is active — that's stored on the user record. When the user is created in multiple companies, calls happen in whichever company is currently selected.

To switch the active company:

Subsequent calls will operate in the newly selected company.

Service users for integrations

For machine-to-machine integrations, create a dedicated Bezala user instead of using a human's credentials. A service user is just a normal user with an email that no person uses; you give it the role it needs (typically accountant or manager for integrations that reach across all employees), and you store its password in your secrets manager.

Why this matters:

  • Real employees leave. When they do, their credentials get rotated and your integration breaks at 3am.

  • Audit trails get cleaner. Every change made by the integration is attributed to the service user, which makes it easy to spot manual changes versus automated ones.

  • Permissions stay scoped. A service user can be limited to one company; a human user might float between several.

SSO and service users

If your company has Google, Microsoft, or Okta SSO enabled, you might have configured Bezala to require SSO for everyone — meaning the password-based POST /api/auth/token endpoint is blocked for users covered by the SSO policy.

For human users, that's the right setting. For an integration, it's a problem: a service user that has to refresh a Google or Microsoft access token before every Bezala call is a lot more moving parts than a service user with a password.

There are three workable ways through:

Option 1 — Use an email outside the SSO domain

The simplest fix. SSO enforcement in Bezala is keyed off the email address. If your SSO domain is @yourcompany.com, create the integration user with an email that isn't covered:

  • A subdomain you don't enrol in SSO, e.g. [email protected].

  • A plus-alias that your IdP doesn't claim, depending on how your SSO is configured: [email protected].

  • An external mailbox you control, e.g. on a shared workspace or a vendor-specific subdomain.

The integration user signs in with email and password through POST /api/auth/token like any non-SSO user, and SSO enforcement doesn't apply because the address isn't in the SSO domain. This is the path most customers end up using.

Option 2 — Mark the user as SSO-exempt

Bezala can flag specific users as exempt from SSO enforcement — typically configured in the company's authentication settings. The exemption is not currently exposed as an API field; it's an admin-level setting. Email [email protected] with the user ID and a one-line justification, and we'll set the exemption.

This is the right path when policy or naming conventions don't let you create an out-of-domain email — for example, you've standardised on every Bezala user living under @yourcompany.com and don't want to make an exception in your IT system.

Option 3 — Authenticate the service user via SSO

Genuinely workable but more complex. You provision the service user in your IdP (creating a Google service account, an Okta service application, or a Microsoft application identity), exchange the IdP's token for a Bezala token via the matching SSO endpoint (/api/auth/google/token, /api/auth/microsoft/token, or /api/auth/okta/token), and refresh as needed.

The downside is that you now have two systems to keep tokens for — your IdP and Bezala — and your integration breaks if either credential expires unexpectedly. The upside, for organisations with strict audit requirements, is that every integration call is traceable through the same identity provider as every human call.

Most integrations don't need this level of rigour. If you can use Option 1, use it.

Security checklist

  • Never embed a token in a public client. Bearer tokens are equivalent to a password. Treat them like one.

  • Store tokens server-side only. Don't put them in mobile-app bundles, browser local storage, or anywhere a user other than the token's owner can see them.

  • Rotate on suspicion. If a token might have leaked, change the user's password — that invalidates the token.

  • Log carefully. Don't log full Authorization headers, even at debug level. Mask them.

  • HTTPS only. Every Bezala endpoint is HTTPS. Don't try to reach it over plain HTTP — there is no plaintext fallback.

Did this answer your question?