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:
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:
/api/external/v2/auth/validate-api-keyValidate 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
| 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 likePOST /deals/{id}/actions/search-lenders. Lender search and similar AI endpoints are not granted bydeals:writealone.
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. |
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 |
{
"request_id": "...",
"error": {
"status": 401,
"type": "unauthorized",
"message": "Invalid or expired authentication token",
"details": {}
}
}