---
title: "Authorization"
description: "The gates every request passes: token scope, organization, subscription, permission, plan limits, country."
url: "https://bareledger.app/guides/api-authorization"
category: "Developers & API"
updated: "2026-11-06"
publisher: "BareLedger (Syneria Technologies OPC)"
---

# Authorization

> The gates every request passes: token scope, organization, subscription, permission, plan limits, country.

Distinct from [API Authentication](https://bareledger.app/guides/api-authentication) (proving who you are) — this page covers what an authenticated caller is actually allowed to do.

## Model

Permission-based, not role-based, at enforcement time. Every tenant-scoped route declares a specific permission key (e.g. `sales.approve`) via a permission check; a role is just a named bundle of permission keys, evaluated once at request time into an effective permission set for the caller. See [Authentication & Authorization](https://bareledger.app/guides/api-authorization) for the full permission catalogue and RBAC matrix, and [Roles & Permissions (RBAC)](https://bareledger.app/guides/roles-permissions) for the module-level operational rules (privilege-ceiling enforcement, self-lockout guards, etc.).

## Layers of enforcement, in order, per request

1. **Authentication** — a valid bearer token (a bearer token).
2. **Tenant resolution** — the `X-Tenant` header must match one of the caller's own active memberships (`tenant:required`); otherwise `409`.
3. **Subscription status** — an active subscription is required for write requests (`subscription` middleware); a lapsed tenant gets `402` on writes (reads and billing routes stay open).
4. **Permission check** — the caller's effective permission set must include the route's required key (`permission:<key>`); otherwise `403` with the missing key named in the response.
5. **Plan limits** — every capability ships on every plan; the only plan gates are record-count limits (user seats at create time, the monthly accounting-transaction cap at posting time), enforced in the service layer with 402 responses. Every active sign-in is a seat, employee-only included; sign-ins beyond the limit become read-only after a 14-day grace (`EnsureSeatWithinPlan` → 402 `seat_over_limit` on writes).
6. **Country gate** — a small number of jurisdiction-specific routes (e.g. Philippines withholding-tax certificates) require the tenant's country to match (`country:<ISO2>`); otherwise `404` (not 403 — the route is presented as not existing).

## Object-level authorization

Beyond the route-level permission check, individual records are scoped to the caller's active tenant automatically — a valid permission key doesn't grant access to another tenant's data. Cross-tenant record lookups return `404`, not `403`, specifically so a response never confirms whether a given record id exists under a tenant the caller doesn't belong to. See Multi-Tenancy & Organization.

## Segregation of duties

Independent of permission keys, every workflow applies the **three-person rule**: the preparer of a record can never review or approve it, and the reviewer can never also approve it — maker, reviewer and approver are three different users. Both checks are enforced server-side on every workflow transition (journals, commercial documents, expenses, payments, payroll runs, approval requests, employee onboardings and profile changes) regardless of permissions held, owner status, or permission combinations such as `sales.approve` + `payment.approve`; a violation is `422` keyed `segregation_of_duties`. Posting to the ledger additionally refuses any entry that isn't approved by a preparer, reviewer and approver who are three distinct users. A tenant needs at least three active members to complete an approval. See [Approval Workflow Engine](https://bareledger.app/guides/approval-workflow).

## Platform-level authorization

A small set of `/admin/*` routes operate outside tenant context entirely, gated by a separate `platform` flag on the user record rather than any tenant permission — see Platform Administration. There is no HTTP path to grant platform-admin status; it's a command-line-only operation.
