# How does authentication work on the VARTA API?

/v1 handlers read the current auth context through current_auth (backend/app/security/deps.py); settings.auth_mode defaults to "audit", in which an unauthenticated request is allowed through, not rejected. An operator exposing /v1 beyond their own network must set enforce mode first.


Read this before you point `/v1` at anything other than your own machine. The
default behaviour is more permissive than an integrator coming from a typical
SaaS API is likely to assume.

## The default mode does not reject unauthenticated requests

Every `/v1` handler resolves the calling identity through `current_auth`
(`backend/app/security/deps.py:24-30`). What that dependency does depends on
one setting: `settings.auth_mode`, which **defaults to `"audit"`**
(`backend/app/core/config.py:26`).

In audit mode, `current_auth` does not reject a request with no credential —
it returns a permissive fallback context (`_AUDIT_FALLBACK`, an admin-role
context on the default tenant) instead of raising
(`backend/app/security/deps.py:17`, `:24-30`). The auth middleware that runs
in front of the handlers behaves the same way: with no principal resolved
from the request, audit mode logs a `would_reject` line and lets the request
continue (`backend/app/security/middleware.py:48-52`).

Put plainly: **on a default self-hosted VARTA instance, `/v1` does not
reject unauthenticated requests.** `POST /v1/calls` — the endpoint that
places a real telephone call — will run for a caller that sends no
`Authorization` header at all. This is a deliberate operational default (the
comment at `deps.py:12-16` explains it exists so an internal console that
doesn't yet send tokens isn't broken by a hard 401 the moment a route is
wired), not a security posture to rely on. If you are exposing your instance
beyond your own network, **set `auth_mode` to `"enforce"` before you do it.**
An integrator who assumes authentication is on by default, and ships a `/v1`
deployment reachable from the internet without checking this setting, has
shipped an unauthenticated call-placing API.

## Audit mode also disables tenant isolation

`auth_mode` does not only gate *authentication*. The same setting gates
**object-level isolation** — which tenant's resources a caller can reach — and
that is the half most integrators miss, because every API page on this site
otherwise describes isolation as if it were unconditional.

Ownership is checked in one place, `require_tenant`
(`backend/app/security/ownership.py:43-59`). Its first act is to opt out:

```python
if not _enforcing():
    return resource                     # audit: observe, don't enforce isolation
```

That early return is `backend/app/security/ownership.py:51-52`, and
`_enforcing()` is just `settings.auth_mode == "enforce"`
(`backend/app/security/ownership.py:17-22`). So on a default instance the
tenant comparison below that line never runs, and the resource is returned to
whoever asked for it.

The consequences are concrete, not theoretical:

- **`GET /v1/agents` lists every tenant's agents.** Its filter is nothing but
  a `require_tenant` call in a `try/except` — an agent is included whenever
  that check doesn't raise (`backend/app/api/v1/agents.py:90-102`). In audit
  mode it never raises, so the "agents visible to your tenant" list is in fact
  the whole instance.
- **A well-formed id belonging to another tenant resolves normally.** It does
  not become [`not_found`](/docs/errors/not_found), and it does not become
  [`tenant_forbidden`](/docs/errors/tenant_forbidden) (which this platform
  never raises in either mode). It just works — sessions, calls and agents
  alike, since they all route through the same `require_tenant`.

Two things follow. First: **the isolation described on the API reference pages
is enforce-mode behaviour**, and each of those pages now says so. Second, and
more sharply — if you are reselling or multi-tenanting a VARTA instance, audit
mode is not a softer security posture, it is the absence of one. Tenant
separation is the property your customers are buying; it is off until you turn
`auth_mode` to `"enforce"`. Verify the setting on the instance itself rather
than assuming a deployment inherited it.

## What enforce mode does

With `settings.auth_mode == "enforce"`:

- `current_auth` raises a `401` with `{"detail": "authentication required"}`
  when no principal was resolved (`backend/app/security/deps.py:28-29`).
- Before a request even reaches a `/v1` handler, `AuthMiddleware` checks for a
  resolved principal itself and, in enforce mode, short-circuits with the
  same `401` and body — `{"detail": "authentication required"}` — with **no**
  error envelope: no `type`, `code`, `request_id` or `doc_url`
  (`backend/app/security/middleware.py:48-50`). That is the form you will
  actually see for most missing-credential requests, because the middleware
  runs first. See [`unauthenticated`](/docs/errors/unauthenticated), which
  documents this bare-body case explicitly — a client should treat that
  response the same as the enveloped `unauthenticated` error the `/v1`
  handlers themselves produce when a request gets further before failing.
- `require_role`, `require_scope` and `require_platform_admin` add role- and
  scope-checks on top of `current_auth`, and — like `current_auth` — only
  actually enforce them in enforce mode (`backend/app/security/deps.py:33-61`).

## The credential the code reads

Send the credential as a standard bearer header:

```
Authorization: Bearer <token>
```

The middleware reads the `Authorization` header, and only accepts the
`Bearer ` scheme — any other scheme (or none) resolves to no principal
(`backend/app/security/principal.py:21-30`). The token itself is a signed,
short-lived JWT minted by the instance's own `TokenService`
(`backend/app/security/tokens.py`) with claim `"typ": "service"` — service
tokens are the only Bearer-token principal type accepted on `/v1`; a channel
token (the type used for the realtime WebSocket path) is deliberately
rejected here even if presented the same way
(`backend/app/security/principal.py:16-30`).

This documentation could not find a `/v1` or studio HTTP endpoint that
mints a service token for you — every mint call in the codebase is either
test code or an internal flow that issues a different token type (a session
cookie for the studio's own login, a channel token for a live call's
WebSocket). Token issuance for external `/v1` callers is an operator
concern on the instance you're talking to, not a self-serve flow this API
exposes today. If you're integrating against someone else's VARTA instance,
ask its operator for a token rather than looking for a signup endpoint; if
you operate the instance yourself, your own deployment tooling is what mints
one.

## What to send

```bash
curl "$VARTA_BASE_URL/agents" \
  -H "Authorization: Bearer $VARTA_API_KEY"
```

Every example on this site uses `$VARTA_API_KEY` as the environment variable
name for this token, regardless of whether the instance you're calling
actually enforces it yet.
