# Authentication

Authenticate with the Comvi REST API using an API key or a session cookie, and see which endpoints accept which.

Canonical: https://comvi.io/docs/api/authentication

Every endpoint requires authentication. Comvi has two methods, and not every endpoint accepts both.

## API keys

Create a key in [app.comvi.io](https://app.comvi.io) under **Settings > API Keys**, pick its scopes, and copy it once. Keys start with `comvi_`. Pass the key as a Bearer token.

```http
Authorization: Bearer comvi_your_api_key_here
```

```bash
curl -H "Authorization: Bearer $COMVI_API_KEY" https://api.comvi.io/v1/project
```

`GET /v1/project` returns the project the key is bound to, which confirms the key works and which project it reaches. See [API Keys](/docs/api/api-keys#get-v1project) for the response. The CLI reads the same key from `.comvirc.json` or `COMVI_API_KEY`.

### Scopes

| Scope | Grants |
|-------|--------|
| `project:read` | Read project metadata, locales, and namespaces |
| `translations:read` | Read and export translation keys, values, glossaries, translation memory, comments, and history |
| `translations:write` | Create and update translation keys, values, namespaces, locales, glossary entries, comments, and imports. Also deletes a namespace and clears translation values |
| `translations:review` | Mark translations reviewed |
| `translations:delete` | Delete translation keys |
| `schema:read` | Read key schema and type-generation metadata |

Every key gets `project:read`, whether or not you ask for it, so the CLI, the fetch loader and the in-context editor can call `GET /v1/project`. You cannot grant a scope you do not hold: the attempt returns `403 FORBIDDEN` with the message `Cannot grant API key permissions that you do not have`. Grant only the scopes an integration needs.

### What a key cannot do

A key is bound to one project and is refused on every organization-scoped route, in one of two ways.

| Routes | Refusal |
|--------|---------|
| Organizations, organization members, invitations (session-only) | `401 AUTHENTICATION_REQUIRED`. The key never reaches the permission check |
| Organization translation-memory routes and the read-only organization glossary routes (term list, single term, three exports) | `403 FORBIDDEN`, message `API keys cannot access organization-scoped routes`, before scopes are looked at |
| Remaining organization glossary routes (session-only) | `401 AUTHENTICATION_REQUIRED` |

A key used against a project other than its own, or whose creator has since lost the underlying permission, gets `403 FORBIDDEN`. A key never has more access than its creator has right now.

## Sessions

Sign in with `POST /v1/auth/login`, sending `email` and `password` as JSON. The response is the current user and a session cookie. Login is rate-limited to 10 requests per minute.

```bash
curl -X POST -H "Content-Type: application/json" \
-d '{"email": "you@example.com", "password": "your-password"}' \
-c cookies.txt https://api.comvi.io/v1/auth/login

curl -b cookies.txt "https://api.comvi.io/v1/projects?organizationId=1"
```

The cookie is `sessionId`, and `__Host-sessionId` in production. It is `HttpOnly`, `SameSite=Strict`, `Path=/`, with no `Domain` attribute, so only `api.comvi.io` reads it. There are no CSRF tokens; `SameSite=Strict` plus the host-bound cookie is the defence.

## Which endpoints accept which

| Rule | Accepts | Where |
|------|---------|-------|
| Session only | The session cookie | Organizations, members, invitations, API key management, creating and deleting projects, adding and removing locales |
| Either | Session cookie or Bearer key | Reading and updating a project, namespaces, translation keys and values, machine translation, glossaries, translation memory, comments, history, webhooks |
| API key only | Bearer key | `GET /v1/project`, `GET /v1/project/locales`, the `/v1/keys/` routes, type-generation |
| API key only, enforced in the handler | Bearer key | The three CLI import/export routes under `/v1/projects/{projectId}/(export\|import/validate\|import/commit)`. The middleware accepts either; the handler refuses a session |

A Bearer token sent to a session-only endpoint does not fall back. The `Authorization` header is never read there, so the request fails with `401 AUTHENTICATION_REQUIRED`.

## Errors

| Status and code | When |
|-----------------|------|
| `401 AUTHENTICATION_REQUIRED` | Missing or invalid credentials, or a Bearer token on a session-only endpoint. Message: `Authentication required. Provide either a valid session or API key.` A session-only route answers the shorter `Authentication required` |
| `401 API_KEY_INVALID`, `API_KEY_DISABLED`, `API_KEY_EXPIRED`, `API_KEY_PERMISSIONS_REVOKED` | A malformed, disabled or expired key, or one whose creator lost the granted permissions, only on API-key-only routes. Routes that accept either method collapse every key failure into `AUTHENTICATION_REQUIRED` |
| `401 UNAUTHORIZED` | No Bearer header on an API-key-only route (message `API key required`), or a session on a CLI import/export route (message `API key authentication required`) |
| `403 FORBIDDEN` | Valid credentials, not enough access. `meta.requiredPermission` names the underlying permission, not the scope; map it back through the scope table. `meta.hint` suggests a new key |

```json
{
"code": "FORBIDDEN",
"message": "API key does not have required permission: translation.edit",
"meta": { "requiredPermission": "translation.edit", "hint": "Please create a new API key with the required permissions" }
}
```

## Next steps

- [API Keys](/docs/api/api-keys) to create and revoke keys over the API
- [Roles and permissions](/docs/platform/roles-permissions) for the permission system behind the scopes
- [Errors](/docs/api/errors) for every code
