Authentication

Secure your API requests with JWT tokens for interactive use or API keys for programmatic access. This guide covers every authentication endpoint with copy-pasteable examples.

Overview

Two authentication methods, one consistent security model

JWT Authentication

Session-based authentication using access tokens and refresh tokens. Access tokens are short-lived (15 minutes). The refresh token is a secure HTTP-only cookie that rotates on every refresh, and a session ends 90 days after sign-in however often it is refreshed. Ideal for web applications and interactive use.

Web / Interactive

API Key Authentication

Long-lived, scoped keys for server-to-server communication. Each key has scopes and can be revoked independently. Keys reach the optimization job endpoints; Monte Carlo, standalone backtests, comparisons, tags and account management need a signed-in session. Ideal for scripts, CI/CD, and backend integrations.

Programmatic / Server

JWT Authentication

Sign up, log in, refresh tokens, and log out using session-based JWT flow

POST
/auth/signup

Create a new user account and send a verification email. The account can sign in at once, but most endpoints answer 403 email_not_verified until the address is confirmed with POST /auth/email/verify.

Request Body
json
{
  "email": "investor@example.com",
  "password": "SecureP@ssw0rd!",
  "display_name": "Jane Doe"
}
Response
200 OK
json
{
  "ok": true,
  "user_id": "3f2b8c1e-6a4d-4b7e-9c2f-1d5e8a7b6c4d"
}

An email that is already registered returns 409 email already registered.

Parameters
FieldTypeDescription
emailstringValid email address (required)
passwordstringAt least 8 characters with a lowercase letter, an uppercase letter and a digit, and not a common password (required)
display_namestringDisplay name for the account (optional)
curl
bash
$ curl -X POST https://api.foliolab.ai/auth/signup \
  -H "Content-Type: application/json" \
  -d '{
    "email": "investor@example.com",
    "password": "SecureP@ssw0rd!",
    "display_name": "Jane Doe"
  }'
Python
python
# Sign up a new user
import requests

response = requests.post(
    "https://api.foliolab.ai/auth/signup",
    json={
        "email": "investor@example.com",
        "password": "SecureP@ssw0rd!",
        "display_name": "Jane Doe",
    },
)
print(response.json())

POST
/auth/login

Authenticate with email and password. Returns an access token in the response body and sets refresh_token and csrf_token as secure HTTP-only cookies.

Request Body
json
{
  "email": "investor@example.com",
  "password": "SecureP@ssw0rd!"
}
Response
200 OK
json
{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "bearer",
  "expires_in": 900
}

The response also sets two cookies: refresh_token (HTTP-only, secure) and csrf_token (readable by JavaScript for CSRF protection). A wrong email or password returns 401 invalid credentials. Google sign-in starts at GET /auth/google and returns to the web app with the same cookies.

curl
bash
$ curl -X POST https://api.foliolab.ai/auth/login \
  -H "Content-Type: application/json" \
  -c cookies.txt \
  -d '{
    "email": "investor@example.com",
    "password": "SecureP@ssw0rd!"
  }'
Python
python
# Log in and persist session cookies
import requests

session = requests.Session()
response = session.post(
    "https://api.foliolab.ai/auth/login",
    json={
        "email": "investor@example.com",
        "password": "SecureP@ssw0rd!",
    },
)
data = response.json()
access_token = data["access_token"]

# Use the token for subsequent requests
headers = {"Authorization": f"Bearer {access_token}"}
print(data)

POST
/auth/refresh

Exchange a valid refresh token (sent as a cookie) for a new access token. Requires the X-CSRF-Token header, which must equal the csrf_token cookie. The refresh token and the CSRF token both rotate, so store the new cookies.

Response
200 OK
json
{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "bearer",
  "expires_in": 900
}
curl
bash
$ curl -X POST https://api.foliolab.ai/auth/refresh \
  -H "X-CSRF-Token: <csrf_token_from_cookie>" \
  -b cookies.txt \
  -c cookies.txt
Python
python
# Refresh the access token using the session cookies
csrf_token = session.cookies.get("csrf_token")

response = session.post(
    "https://api.foliolab.ai/auth/refresh",
    headers={"X-CSRF-Token": csrf_token},
)
new_token = response.json()["access_token"]
print(f"New access token: {new_token[:20]}...")

POST
/auth/logout

Revoke the current session. Requires the X-CSRF-Token header.

Response
200 OK
json
{
  "ok": true
}
curl
bash
$ curl -X POST https://api.foliolab.ai/auth/logout \
  -H "X-CSRF-Token: <csrf_token_from_cookie>" \
  -b cookies.txt
Python
python
# Log out and clear the session
csrf_token = session.cookies.get("csrf_token")

response = session.post(
    "https://api.foliolab.ai/auth/logout",
    headers={"X-CSRF-Token": csrf_token},
)
print(response.json())  # {"ok": true}

API Key Authentication

Create and manage long-lived API keys for programmatic access

POST
/auth/api-clients

Register a new API client application. Each client can have up to five active API keys, and an account can have up to ten clients. Requires JWT authentication. GET /auth/api-clients lists your clients, and PATCH /auth/api-clients/{client_id} renames one or sets is_disabled. DELETE /auth/api-clients/{client_id} revokes every key of the client and deletes it.

Request Body
json
{
  "name": "My Trading Bot"
}
Response
200 OK
json
{
  "client_id": "7c1d2e3f-4a5b-4c6d-8e9f-0a1b2c3d4e5f",
  "name": "My Trading Bot"
}
curl
bash
$ curl -X POST https://api.foliolab.ai/auth/api-clients \
  -H "Authorization: Bearer <access_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "My Trading Bot"
  }'
Python
python
# Create an API client
response = requests.post(
    "https://api.foliolab.ai/auth/api-clients",
    headers={"Authorization": f"Bearer {access_token}"},
    json={"name": "My Trading Bot"},
)
client = response.json()
print(f"Client ID: {client['client_id']}")

POST
/auth/api-keys

Generate a new API key for an existing client. You can assign scopes to control what the key can access. Omit scopes to get jobs:read and jobs:write. GET /auth/api-keys?client_id=... lists a client's keys by prefix, PATCH /auth/api-keys/{api_key_id} revokes a key, and DELETE /auth/api-keys/{api_key_id} deletes it.

Request Body
json
{
  "client_id": "7c1d2e3f-4a5b-4c6d-8e9f-0a1b2c3d4e5f",
  "scopes": ["jobs:read", "jobs:write"]
}
Available Scopes
ScopeDescription
jobs:readView job status, history, results, configurations and artifacts
jobs:writeSubmit, rename, cancel and delete optimization jobs and request PDF reports. Includes jobs:read
jobs:* or *Every jobs scope
Response
200 OK
json
{
  "api_key_id": "9e8d7c6b-5a4f-4e3d-2c1b-0a9f8e7d6c5b",
  "api_key": "ipo_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
  "scopes": ["jobs:read", "jobs:write"]
}

Important: The api_key field is only returned once at creation time. Store it securely immediately: you will not be able to retrieve it again.

curl
bash
$ curl -X POST https://api.foliolab.ai/auth/api-keys \
  -H "Authorization: Bearer <access_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "7c1d2e3f-4a5b-4c6d-8e9f-0a1b2c3d4e5f",
    "scopes": ["jobs:read", "jobs:write"]
  }'
Python
python
# Generate an API key with scoped permissions
response = requests.post(
    "https://api.foliolab.ai/auth/api-keys",
    headers={"Authorization": f"Bearer {access_token}"},
    json={
        "client_id": client["client_id"],
        "scopes": ["jobs:read", "jobs:write"],
    },
)
api_key_data = response.json()

# IMPORTANT: Save this key securely. It is only shown once.
api_key = api_key_data["api_key"]
print(f"API Key: {api_key}")

Using API Keys

Once you have an API key, include it in your requests using either of these two header formats. A key without the scope a route needs receives 403 API key lacks required scope, and a revoked key receives 401 API key revoked.

Option 1: Authorization Header (Recommended)
http
Authorization: Bearer ipo_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
Option 2: X-API-Key Header
http
X-API-Key: ipo_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
curl
bash
$ curl "https://api.foliolab.ai/jobs/history?limit=10" \
  -H "Authorization: Bearer ipo_live_a1b2c3d4..."
Python
python
# Using an API key for all requests
import requests

API_KEY = "ipo_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"

# Option 1: Authorization header
response = requests.get(
    "https://api.foliolab.ai/jobs/history",
    headers={"Authorization": f"Bearer {API_KEY}"},
)

# Option 2: X-API-Key header
response = requests.get(
    "https://api.foliolab.ai/jobs/history",
    headers={"X-API-Key": API_KEY},
)

print(response.json())

Password Reset

Self-service password recovery via email verification

POST
/auth/password/reset/request

Request a password reset email. A one-time reset token, valid for two hours, is sent to the provided email address if it is associated with an account.

Request Body
json
{
  "email": "investor@example.com"
}
Response
200 OK
json
{
  "ok": true,
  "detail": "If the account exists, a reset token was issued"
}

The response is intentionally identical whether or not the email exists, to prevent account enumeration.

curl
bash
$ curl -X POST https://api.foliolab.ai/auth/password/reset/request \
  -H "Content-Type: application/json" \
  -d '{"email": "investor@example.com"}'
Python
python
# Request a password reset
response = requests.post(
    "https://api.foliolab.ai/auth/password/reset/request",
    json={"email": "investor@example.com"},
)
print(response.json())

POST
/auth/password/reset/confirm

Confirm the password reset using the token received via email. Sets the new password, invalidates the reset token, and signs out every existing session and connected AI assistant. The new password follows the same rules as sign-up. A signed-in user changes a password with POST /auth/password/change and current_password plus new_password.

Request Body
json
{
  "token": "rst_a1b2c3d4e5f6...",
  "new_password": "NewSecureP@ss!"
}
Response
200 OK
json
{
  "ok": true
}
curl
bash
$ curl -X POST https://api.foliolab.ai/auth/password/reset/confirm \
  -H "Content-Type: application/json" \
  -d '{
    "token": "rst_a1b2c3d4e5f6...",
    "new_password": "NewSecureP@ss!"
  }'
Python
python
# Confirm the password reset
response = requests.post(
    "https://api.foliolab.ai/auth/password/reset/confirm",
    json={
        "token": "rst_a1b2c3d4e5f6...",
        "new_password": "NewSecureP@ss!",
    },
)
print(response.json())

Security Best Practices

Recommendations for keeping your integration secure

  • 1Never expose API keys in client-side code. API keys should only be used in server-side applications, backend services, or CI/CD pipelines. Never embed them in frontend JavaScript, mobile apps, or public repositories.
  • 2Use the principle of least privilege. When creating API keys, assign only the scopes your application actually needs. A read-only dashboard should use jobs:read only.
  • 3Rotate keys regularly. Create new API keys periodically and revoke old ones. This limits the impact of any key that may have been compromised.
  • 4Store secrets in environment variables. Use environment variables or a secrets manager (e.g., AWS Secrets Manager, HashiCorp Vault) instead of hardcoding credentials in your source code.
  • 5Always use HTTPS. All API requests must be made over HTTPS. The API rejects plaintext HTTP connections to prevent credentials from being transmitted in the clear.
  • 6Handle token refresh gracefully. Implement automatic token refresh in your application. When you receive a 401 response, attempt a silent refresh before prompting the user to re-authenticate.
  • 7Include CSRF tokens on state-changing requests. Any POST, PUT, PATCH or DELETE request that carries the session cookies must include the X-CSRF-Token header, equal to the csrf_token cookie, or it receives 403 CSRF validation failed. A request authenticated only by a bearer token or API key, with no cookies, needs no CSRF header.

Not investment advice. Past performance is not indicative of future results.