# Authentication

> Learn how to authenticate with the Lev API using API keys or JWT tokens.

Source: https://www.lev.com/docs/build/authentication

Last updated: May 2026

---
## Overview

| Method | Use Case | Token Lifetime |
|--------|----------|----------------|
| **API Key** | Server-to-server, CI/CD, automation | Long-lived (until revoked) |
| **JWT (Auth0)** | Interactive clients, MCP servers | Short-lived (configurable) |

Both methods use the `Authorization: Bearer <token>` header. The API determines the token type automatically.

## API Key Authentication

API keys are the simplest way to authenticate. Create a key via the API or the Lev platform, then include it in every request:

```bash
curl -X GET "https://api.lev.com/api/external/v2/deals" \
  -H "Authorization: Bearer lev_sk_abc123def456..." \
  -H "X-Origin-App: my-integration"
```

API keys are scoped to a specific user and account. The key inherits the user's permissions — a key cannot access resources the user doesn't have access to.

### Key validation

You can validate a key programmatically to check its scopes and associated account:

### `POST /api/external/v2/auth/validate-api-key`

Validate an API key (unauthenticated endpoint)

## JWT Authentication

For interactive applications (like MCP servers connecting via OAuth), the API accepts Auth0-issued JWT tokens:

```bash
curl -X GET "https://api.lev.com/api/external/v2/deals" \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." \
  -H "X-Origin-App: claude-desktop"
```

JWT tokens are obtained through the OAuth 2.0 + PKCE flow. The MCP server handles this automatically for MCP clients like Claude Desktop and Cursor.

## Scopes

Every endpoint is gated by a required scope. If your credential doesn't carry the required scope, the request returns `403 Forbidden` with `type: "forbidden"`. You can see the scopes granted to the current session in the `platform.granted_scopes` field of the [scoped `GET /me` response](/build/account#current-user), or in the `scopes` field of the `POST /auth/validate-api-key` response.

**Granular scopes**

| Domain | Read | Write |
|--------|------|-------|
| Account | `account:read` | `account:write` |
| Checklists | `checklists:read` | `checklists:write` |
| Companies | `companies:read` | `companies:write` |
| Contacts | `contacts:read` | `contacts:write` |
| Deals | `deals:read` | `deals:write` |
| Documents | `documents:read` | `documents:write` |
| Lender directory | `lenders:read` | — |
| Market data | `market:read` | — |
| Pipelines | `pipelines:read` | `pipelines:write` |
| Placements | `placements:read` | `placements:write` |
| Term sheets | `termsheets:read` | `termsheets:write` |

Plus one action-gated scope:

- `ai:actions` — required for AI-powered actions. These endpoints are not granted by `deals:write` alone.

**Permission presets on API keys**

When you create an API key, you pick a permission level. The backend maps the level to a scope set automatically:

| Permission | Scopes granted |
|------------|----------------|
| `full_access` (default) | Every scope above, including `ai:actions` |
| `read_only` | Every `*:read` scope. No write scopes, no `ai:actions`. |

JWT users receive the same scope surface as `full_access`, further filtered by their Lev role at the resource level — a key or JWT cannot access data the underlying user can't see in the app.

## Required Headers

Every request must include:

| Header | Required | Description |
|--------|----------|-------------|
| `Authorization` | Yes | `Bearer <api_key_or_jwt>` |
| `X-Origin-App` | Yes | Identifies the calling application (e.g., `my-integration`, `claude-desktop`) |
| `X-Active-Account` | For multi-account JWT users | Account `slug` to scope the request to. Required when the authenticated user belongs to more than one account; optional for single-account users. See [Multi-Account Access](/build/account#multi-account-access). |
| `Content-Type` | For POST/PATCH | `application/json` |
| `Idempotency-Key` | Recommended for writes | UUID to prevent duplicate operations (24-hour expiry) |

  API keys are bound to a specific account at issuance, so the account is already implied on every request. The backend does not read `X-Active-Account` when the caller authenticates with an API key — passing it has no effect. The header only applies to JWT-authenticated users who belong to more than one account.

## Authentication Errors

| Status | Meaning |
|--------|---------|
| `401 Unauthorized` | Missing, invalid, or expired token |
| `403 Forbidden` | Valid token but insufficient permissions for this resource |

```json
{
  "request_id": "...",
  "error": {
    "status": 401,
    "type": "unauthorized",
    "message": "Invalid or expired authentication token",
    "details": {}
  }
}
```