Build

Authentication

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

Updated March 2026

Overview

MethodUse CaseToken Lifetime
API KeyServer-to-server, CI/CD, automationLong-lived (until revoked)
JWT (Auth0)Interactive clients, MCP serversShort-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:

curl -X GET "https://api.levcapital.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)

{
  "api_key": "lev_sk_abc123def456..."
}

Returns { "valid": true, "user_id": ..., "account_id": ..., "scopes": [...], "tier": "..." } if valid, or { "valid": false } if invalid.

JWT Authentication

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

curl -X GET "https://api.levcapital.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 v2 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, or in the scopes field of the POST /auth/validate-api-key response.

Granular scopes

DomainReadWrite
Accountaccount:readaccount:write
Checklistschecklists:readchecklists:write
Companiescompanies:readcompanies:write
Contactscontacts:readcontacts:write
Dealsdeals:readdeals:write
Documentsdocuments:readdocuments:write
Lender directorylenders:read
Market datamarket:read
Pipelinespipelines:readpipelines:write
Placementsplacements:readplacements:write
Term sheetstermsheets:readtermsheets:write

Plus one action-gated scope:

  • ai:actions — required for AI-powered actions like POST /deals/{id}/actions/search-lenders. Lender search and similar AI 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:

PermissionScopes granted
full_access (default)Every scope above, including ai:actions
read_onlyEvery *: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:

HeaderRequiredDescription
AuthorizationYesBearer <api_key_or_jwt>
X-Origin-AppYesIdentifies the calling application (e.g., my-integration, claude-desktop)
X-Active-AccountFor multi-account JWT usersAccount 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.
Content-TypeFor POST/PATCHapplication/json
Idempotency-KeyRecommended for writesUUID to prevent duplicate operations (24-hour expiry)
API keys don't read X-Active-Account

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

StatusMeaning
401 UnauthorizedMissing, invalid, or expired token
403 ForbiddenValid token but insufficient permissions for this resource
{
  "request_id": "...",
  "error": {
    "status": 401,
    "type": "unauthorized",
    "message": "Invalid or expired authentication token",
    "details": {}
  }
}
More in this section