Skip to main content

Authentication

Version: 1.0 Last Updated: 2026-02-03 Security Level: Production-Ready


Table of Contents​

  1. Overview
  2. Authentication Methods
  3. JWT Token Authentication
  4. API Key Authentication
  5. OAuth2 Integration
  6. Security Best Practices
  7. Token Management
  8. Troubleshooting

Overview​

AINative Studio uses industry-standard authentication mechanisms to secure API access. We support multiple authentication methods to accommodate different use cases:

  • JWT Bearer Tokens: For user-facing applications and session management
  • API Keys: For server-to-server communication and long-lived integrations
  • OAuth2 / Sign in with AINative: For third-party app integrations — let your users authenticate with their AINative account

Authentication Flow Diagram​

User Registration/Login
│
▼
Generate JWT Token
│
├──────> Store in localStorage (web)
├──────> Store in keychain (mobile)
└──────> Store in memory (server)
│
▼
API Request with Token
│
▼
Backend Validates Token
│
├──────> Valid ──────> Process Request
│
└──────> Invalid ────> 401 Unauthorized

Authentication Methods​

Comparison Table​

MethodUse CaseExpirationRevocableBest For
JWT TokenUser sessions8 hoursYes (blacklist)Web/mobile apps
API KeyService integration90 days (configurable)YesServer-to-server
OAuth2Third-party authN/AYesSocial login

JWT Token Authentication​

How JWT Tokens Work​

JSON Web Tokens (JWT) are self-contained tokens that include:

  1. Header: Algorithm and token type
  2. Payload: User claims (id, email, role, expiration)
  3. Signature: Cryptographic signature to verify authenticity

Token Structure​

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 <- Header
.eyJzdWIiOiI1NTBlODQwMC1lMjliLTQxZDQtYTcxNi00NDY2NTU0NDAwMDAiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJyb2xlIjoiVVNFUiIsImV4cCI6MTcwNDExMDQwMH0 <- Payload
.xyz123abc <- Signature

Decoded Payload​

{
"sub": "550e8400-e29b-41d4-a716-446655440000", // User ID
"email": "user@example.com",
"role": "USER",
"exp": 1704110400, // Expiration timestamp
"iat": 1704082400 // Issued at timestamp
}

Obtaining a JWT Token​

Method 1: Email/Password Registration​

POST /api/v1/auth/register
Content-Type: application/json

{
"email": "user@example.com",
"password": "SecurePass123!",
"full_name": "John Doe"
}

Response:

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer",
"expires_in": 28800
}

Method 2: Email/Password Login​

POST /api/v1/auth/login
Content-Type: application/json

{
"email": "user@example.com",
"password": "SecurePass123!"
}

Response:

{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer",
"expires_in": 28800
}

Using JWT Tokens​

Include the token in the Authorization header:

GET /api/v1/auth/me
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Python Example​

import requests

## Login and get token
login_response = requests.post(
"https://api.ainative.studio/api/v1/auth/login",
json={
"email": "user@example.com",
"password": "SecurePass123!"
}
)

token = login_response.json()["access_token"]

## Use token for authenticated requests
headers = {"Authorization": f"Bearer {token}"}

user_response = requests.get(
"https://api.ainative.studio/api/v1/auth/me",
headers=headers
)

print(user_response.json())

JavaScript Example​

// Login and get token
const loginResponse = await fetch(
'https://api.ainative.studio/api/v1/auth/login',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: 'user@example.com',
password: 'SecurePass123!'
})
}
);

const { access_token } = await loginResponse.json();

// Use token for authenticated requests
const userResponse = await fetch(
'https://api.ainative.studio/api/v1/auth/me',
{
headers: { 'Authorization': `Bearer ${access_token}` }
}
);

const userData = await userResponse.json();
console.log(userData);

Token Refresh​

Tokens expire after 8 hours. Refresh before expiration:

POST /api/v1/auth/refresh
Authorization: Bearer <current_token>

Response:

{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer",
"expires_in": 28800
}

Automatic Token Refresh (Python)​

import requests
from datetime import datetime, timedelta

class AuthClient:
def __init__(self, email, password):
self.email = email
self.password = password
self.token = None
self.token_expires_at = None
self.login()

def login(self):
response = requests.post(
"https://api.ainative.studio/api/v1/auth/login",
json={"email": self.email, "password": self.password}
)
data = response.json()
self.token = data["access_token"]
self.token_expires_at = datetime.now() + timedelta(seconds=data["expires_in"])

def get_token(self):
# Refresh 5 minutes before expiration
if datetime.now() >= self.token_expires_at - timedelta(minutes=5):
self.refresh_token()
return self.token

def refresh_token(self):
response = requests.post(
"https://api.ainative.studio/api/v1/auth/refresh",
headers={"Authorization": f"Bearer {self.token}"}
)
data = response.json()
self.token = data["access_token"]
self.token_expires_at = datetime.now() + timedelta(seconds=data["expires_in"])

def make_request(self, url, **kwargs):
headers = kwargs.get("headers", {})
headers["Authorization"] = f"Bearer {self.get_token()}"
kwargs["headers"] = headers
return requests.get(url, **kwargs)

## Usage
client = AuthClient("user@example.com", "SecurePass123!")
response = client.make_request("https://api.ainative.studio/api/v1/database/projects")
print(response.json())

API Key Authentication​

When to Use API Keys​

Use API keys for:

  • Server-to-server communication
  • CI/CD pipelines
  • Long-running services
  • Background jobs
  • Webhook endpoints

Do NOT use API keys for:

  • Client-side JavaScript (keys will be exposed)
  • Mobile apps (use JWT tokens instead)
  • User-facing applications

Creating an API Key​

POST /api/v1/api-keys
Authorization: Bearer YOUR_JWT_TOKEN
Content-Type: application/json

{
"name": "Production Server Key",
"expires_in_days": 90
}

Response:

{
"id": "key_550e8400-e29b-41d4-a716-446655440000",
"api_key": "sk_live_EXAMPLE_not_a_real_key",
"name": "Production Server Key",
"prefix": "sk_live",
"expires_at": "2026-05-03T00:00:00Z",
"created_at": "2026-02-03T12:00:00Z"
}

Critical: Save the api_key value immediately - it's only shown once!

API Key Formats​

PrefixEnvironmentUsage
sk_ / sk_live_ProductionAuthenticated users — permanent keys scoped to a project
sk_test_TestingSandbox environment
tmp_TemporaryAuto-provisioned for unauthenticated instant-db users — expires in 72 hours
zdb_live_ClaimedIssued when a tmp_ project is claimed to a real account

Using API Keys​

GET /api/v1/database/projects
X-API-Key: sk_live_EXAMPLE_not_a_real_key

Option 2: Authorization Header​

GET /api/v1/database/projects
Authorization: Bearer sk_live_EXAMPLE_not_a_real_key

Python Example with API Key​

import requests
import os

API_KEY = os.getenv("AINATIVE_API_KEY")

headers = {"X-API-Key": API_KEY}

response = requests.get(
"https://api.ainative.studio/api/v1/database/projects",
headers=headers
)

print(response.json())

Verifying an API Key​

Before running a paid operation (for example, chat completions), you can pre-check a key's status, tier, remaining credits, and capabilities. This is the canonical API-key verification endpoint — apps and agents should use it instead of rolling their own key validation.

POST /api/v1/public/api-keys/verify
X-API-Key: sk_live_EXAMPLE_not_a_real_key

The key is read from the request headers — pass it as X-API-Key or as Authorization: Bearer sk_.... No request body is required.

Response:

{
"status": "READY",
"message": null,
"email": "user@example.com",
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"organization_id": "0f244da9-...",
"organization_name": "Acme Inc",
"tier": "pro",
"user_plan": "pro",
"subscription": {
"tier": "PRO",
"status": "active",
"credits_total": 100000,
"credits_remaining": 84568
},
"capabilities": {
"chat_completions": true,
"embeddings": true,
"zerodb": true,
"file_upload": true
}
}

The status field tells you whether the key can be used:

statusMeaning
READYKey is valid and can perform all operations for its tier
insufficient_creditsPaid tier but credits are exhausted — upgrade or wait for reset
trial_activeTemporary Instant DB key — register to keep data and unlock chat
trial_expiredInstant DB trial expired (72h) — claim the account before data is deleted
upgrade_requiredFree tier — a paid plan is required for chat completions

An invalid or expired key returns 401 Unauthorized (the request never reaches the verify logic).

Managing API Keys​

List All Your API Keys​

GET /api/v1/api-keys
Authorization: Bearer YOUR_JWT_TOKEN

Response:

[
{
"id": "key_123",
"name": "Production Server Key",
"prefix": "sk_live",
"is_active": true,
"last_used_at": "2026-02-03T11:45:00Z",
"usage_count": 15432,
"expires_at": "2026-05-03T00:00:00Z",
"created_at": "2026-02-03T12:00:00Z"
}
]

Revoke an API Key​

DELETE /api/v1/api-keys/{key_id}
Authorization: Bearer YOUR_JWT_TOKEN

Response:

{
"message": "API key revoked successfully"
}

API Key Rotation Strategy​

Best practice: Rotate keys every 90 days

import requests
from datetime import datetime, timedelta

def rotate_api_key(old_key_id: str, jwt_token: str):
"""
Rotate API key: create new key, update services, revoke old key
"""
# Step 1: Create new key
create_response = requests.post(
"https://api.ainative.studio/api/v1/api-keys",
headers={"Authorization": f"Bearer {jwt_token}"},
json={"name": f"Rotated Key {datetime.now()}", "expires_in_days": 90}
)
new_key = create_response.json()["api_key"]

print(f"New API Key: {new_key}")
print("Update your services with this new key, then press Enter to revoke the old key...")
input()

# Step 2: Revoke old key
revoke_response = requests.delete(
f"https://api.ainative.studio/api/v1/api-keys/{old_key_id}",
headers={"Authorization": f"Bearer {jwt_token}"}
)

print("Old key revoked successfully")
return new_key

OAuth2 Integration​

Supported OAuth Providers​

  • AINative (Sign in with AINative) — OAuth 2.1 + PKCE for third-party apps
  • GitHub
  • LinkedIn

Sign in with AINative​

Allow your users to sign in with their AINative account. AINative implements OAuth 2.1 Authorization Code flow with PKCE and publishes a standard OIDC discovery document.

OIDC Discovery​

GET https://api.ainative.studio/.well-known/openid-configuration

OIDC-compatible libraries (NextAuth.js, Auth.js, Passport.js) can auto-configure from this URL alone.

Step 1: Register Your OAuth Client​

Register once to receive a client_id and client_secret.

curl -X POST https://api.ainative.studio/oauth/clients/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "Your App Name",
"redirect_uris": ["https://yourapp.com/api/auth/callback/ainative"],
"grant_types": ["authorization_code"],
"scope": "openid profile email",
"token_endpoint_auth_method": "client_secret_basic"
}'
{
"client_id": "550e8400-e29b-41d4-a716-446655440000",
"client_secret": "your-secret-here",
"redirect_uris": ["https://yourapp.com/api/auth/callback/ainative"],
"scope": "openid profile email"
}

Store both values in environment variables. Never expose client_secret in client-side code.

Step 2: Generate PKCE Values (server-side)​

Python:

import secrets, hashlib, base64

code_verifier = secrets.token_urlsafe(64)
digest = hashlib.sha256(code_verifier.encode()).digest()
code_challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode()

TypeScript:

import crypto from "crypto";

const codeVerifier = crypto.randomBytes(64).toString("base64url");
const codeChallenge = crypto
.createHash("sha256")
.update(codeVerifier)
.digest("base64url");

Step 3: Redirect to AINative​

GET https://api.ainative.studio/oauth/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://yourapp.com/api/auth/callback/ainative
&scope=openid%20profile%20email
&state=RANDOM_CSRF_TOKEN
&code_challenge=YOUR_CODE_CHALLENGE
&code_challenge_method=S256

Step 4: Exchange Code for Token​

curl -X POST https://api.ainative.studio/api/v1/oauth/token \
-u "CLIENT_ID:CLIENT_SECRET" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code&code=AUTH_CODE&redirect_uri=YOUR_REDIRECT_URI&code_verifier=CODE_VERIFIER"
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer",
"expires_in": 28800
}

Step 5: Fetch User Info​

curl https://api.ainative.studio/oauth/userinfo \
-H "Authorization: Bearer ACCESS_TOKEN"
{
"sub": "9492c823-e54e-4a5b-915e-593781d6b805",
"email": "user@example.com",
"email_verified": true,
"name": "Jane Developer",
"given_name": "Jane",
"family_name": "Developer",
"plan": "pro"
}

Use sub as your stable user identifier — it never changes.

Available Scopes​

ScopeDescription
openidRequired for OIDC — enables sub claim
profilename, given_name, family_name
emailemail, email_verified
memory:readRead from the user's ZeroDB memory
memory:writeWrite to the user's ZeroDB memory
vector:searchQuery the user's vector store
vector:writeIndex documents into the user's vector store
agent:readList and inspect the user's agents
agent:writeCreate and deploy agents on the user's behalf

NextAuth.js Integration​

// app/api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth";

export const { handlers, auth } = NextAuth({
providers: [
{
id: "ainative",
name: "AINative",
type: "oauth",
issuer: "https://api.ainative.studio",
wellKnown: "https://api.ainative.studio/.well-known/openid-configuration",
authorization: { params: { scope: "openid profile email" } },
clientId: process.env.AINATIVE_CLIENT_ID,
clientSecret: process.env.AINATIVE_CLIENT_SECRET,
profile(profile) {
return {
id: profile.sub,
name: profile.name,
email: profile.email,
};
},
},
],
});

Endpoint Reference​

EndpointMethodDescription
/oauth/clients/registerPOSTRegister OAuth client (RFC 7591)
/oauth/authorizeGETAuthorization + login page
/api/v1/oauth/tokenPOSTExchange code for access token
/oauth/userinfoGETOIDC UserInfo claims
/.well-known/openid-configurationGETOIDC discovery document

GitHub OAuth Flow​

Step 1: Redirect User to GitHub​

https://github.com/login/oauth/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=YOUR_REDIRECT_URI&scope=user:email

Step 2: Exchange Code for Token​

POST /api/v1/auth/github/callback
Content-Type: application/json

{
"code": "github_authorization_code",
"redirect_uri": "https://yourapp.com/callback"
}

Response:

{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer",
"user": {
"id": "github_user_id",
"email": "user@example.com",
"name": "John Doe",
"avatar_url": "https://avatars.githubusercontent.com/..."
}
}

LinkedIn OAuth Flow​

Step 1: Redirect User to LinkedIn​

https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=YOUR_REDIRECT_URI&scope=openid%20profile%20email

Step 2: Exchange Code for Token​

POST /api/v1/auth/linkedin/callback
Content-Type: application/json

{
"code": "linkedin_authorization_code",
"redirect_uri": "https://yourapp.com/callback"
}

Response:

{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer",
"expires_in": 28800,
"user": {
"id": "linkedin_user_id",
"email": "user@example.com",
"name": "John Doe",
"picture": "https://media.licdn.com/..."
}
}

Security Best Practices​

1. Token Storage​

PlatformRecommended Storage
Web (Browser)localStorage or sessionStorage (XSS risk), prefer secure HttpOnly cookies
Mobile (iOS/Android)Keychain (iOS) or KeyStore (Android)
ServerEnvironment variables or secrets manager
Desktop AppOS-specific secure storage

2. Never Commit Credentials​

## .gitignore
.env
.env.local
*.pem
*.key
config/secrets.json

3. Use Environment Variables​

## ❌ WRONG - Hardcoded
API_KEY = "sk_live_EXAMPLE_not_a_real_key"

## ✅ CORRECT - Environment variable
import os
API_KEY = os.getenv("AINATIVE_API_KEY")

4. Implement Token Expiration Handling​

import requests

def make_authenticated_request(url, token):
try:
response = requests.get(
url,
headers={"Authorization": f"Bearer {token}"}
)
response.raise_for_status()
return response.json()
except requests.exceptions.HTTPError as e:
if e.response.status_code == 401:
# Token expired - refresh or re-login
new_token = refresh_token(token)
return make_authenticated_request(url, new_token)
raise

5. Use HTTPS Only​

## ❌ WRONG
base_url = "http://api.ainative.studio"

## ✅ CORRECT
base_url = "https://api.ainative.studio"

6. Implement Rate Limiting​

from time import sleep
import requests

class RateLimitedClient:
def __init__(self, api_key, requests_per_minute=60):
self.api_key = api_key
self.requests_per_minute = requests_per_minute
self.request_times = []

def make_request(self, url, **kwargs):
# Remove old requests from tracking
current_time = time.time()
self.request_times = [
t for t in self.request_times
if current_time - t < 60
]

# Wait if rate limit reached
if len(self.request_times) >= self.requests_per_minute:
sleep_time = 60 - (current_time - self.request_times[0])
sleep(sleep_time)

# Make request
headers = kwargs.get("headers", {})
headers["X-API-Key"] = self.api_key
kwargs["headers"] = headers

response = requests.get(url, **kwargs)
self.request_times.append(time.time())

return response

7. Validate SSL Certificates​

import requests

## ✅ Always verify SSL (default)
response = requests.get(url, verify=True)

## ❌ NEVER do this in production
response = requests.get(url, verify=False) # Insecure!

8. Password Requirements​

When creating user accounts:

  • Minimum 8 characters
  • At least one uppercase letter (A-Z)
  • At least one lowercase letter (a-z)
  • At least one number (0-9)
  • At least one special character (!@#$%^&*)

Token Management​

Token Blacklist​

Logout invalidates tokens by adding them to a blacklist:

POST /api/v1/auth/logout
Authorization: Bearer <token>

Password Reset Flow​

Step 1: Request Reset​

POST /api/v1/auth/forgot-password
Content-Type: application/json

{
"email": "user@example.com"
}

Step 2: Reset with Token from Email​

POST /api/v1/auth/reset-password
Content-Type: application/json

{
"token": "reset_token_from_email",
"new_password": "NewSecurePass456!"
}

Change Password​

POST /api/v1/auth/change-password
Authorization: Bearer <token>
Content-Type: application/json

{
"current_password": "OldPass123!",
"new_password": "NewPass456!"
}

Troubleshooting​

Common Issues​

1. 401 Unauthorized​

Cause: Invalid or expired token

Solution:

## Refresh token or re-login
response = requests.post(
"https://api.ainative.studio/api/v1/auth/refresh",
headers={"Authorization": f"Bearer {token}"}
)
new_token = response.json()["access_token"]

2. 403 Forbidden​

Cause: Insufficient permissions or wrong API key for the workspace

Solution: Check the error response — it includes workspace_name and suggested_api_key to help you switch to the correct key. This often happens when using a tmp_ key after signing up; use your project-scoped sk_ or zdb_live_ key instead.

3. Token Not Working After Logout​

Cause: Token is blacklisted

Solution: Generate a new token by logging in again

4. API Key Invalid​

Cause: Key was revoked or expired

Solution: Create a new API key


Identity Merge​

If you started with a temporary instant-db (tmp_ key) and later created a real account, merge the temp identity into your real account:

POST /api/v1/auth/merge-identity
Authorization: Bearer YOUR_JWT_TOKEN
Content-Type: application/json

{
"temp_api_key": "tmp_your_temp_key..."
}

This transfers all projects and API keys from the temp identity to your authenticated account and deactivates the temp user. All data is preserved.


Auto-Provisioning (Stripe)​

When a user signs up through Stripe checkout, AINative auto-provisions their account:

  • Creates the user with a bcrypt-hashed temporary password
  • Creates a Default Workspace and assigns the USER role
  • Sends a set-password email and schedules a 24h welcome email
  • Syncs to CRM

When the user later registers with POST /api/v1/auth/register and includes the stripe_session_id, the existing auto-provisioned account is detected and updated — no duplicate account is created.


Testing Authentication​

Test Endpoints​

## Test authentication status
GET /api/v1/test-auth/auth-status

## Create test user
POST /api/v1/test-auth/create-test-user
Content-Type: application/json
{
"email": "test@example.com",
"password": "TestPass123!"
}

## Test login
POST /api/v1/test-auth/test-login
Content-Type: application/json
{
"email": "test@example.com",
"password": "TestPass123!"
}

Additional Resources​


Last Updated: 2026-02-03 Version: 1.0 Security Review: Completed

Refs #1015