Catalio provides enterprise-grade authentication and authorization for API access. This comprehensive guide covers OAuth 2.0 flows, JWT token management, API key authentication, multi-tenant authorization, role-based access control, and security best practices.
Table of Contents
- Authentication Overview
- OAuth 2.0 Authentication
- API Key Authentication
- JWT Token Structure
- Multi-Tenant Authorization
- Role-Based Access Control
- Making Authenticated Requests
- Security Best Practices
- Rate Limiting
- Common Authentication Errors
Authentication Overview
Supported Authentication Methods
Catalio supports two primary authentication methods for API access:
1. OAuth 2.0 with Auth0 (Recommended for Web Applications)
- Industry-standard OAuth 2.0 authorization code flow
- Secure delegation of user credentials
- Automatic token refresh
- Single sign-on (SSO) support
- Ideal for web applications and mobile apps
2. API Keys (Recommended for Server-to-Server)
- Long-lived authentication tokens
- Programmatic access for automation and integrations
- Ideal for CI/CD pipelines, MCP clients, and background services
- Scoped to individual users for audit tracking
Authentication Architecture
┌──────────────┐
│ Client │
│ Application │
└──────┬───────┘
│
│ 1. Authenticate
▼
┌──────────────────┐
│ Auth0 / API Key │
│ Validation │
└──────┬───────────┘
│
│ 2. Issue JWT Token
▼
┌──────────────────┐
│ Catalio API │
│ Authorization │
└──────┬───────────┘
│
│ 3. Enforce Policies
▼
┌──────────────────┐
│ Resource │
│ Access │
└──────────────────┘
Authentication vs. Authorization
Authentication verifies who you are:
- Validates credentials (OAuth tokens, API keys)
- Identifies the user making the request
- Establishes actor context for operations
Authorization determines what you can do:
- Multi-tenant data isolation
- Role-based access control (RBAC)
- Action-level permissions
- Resource-specific policies
OAuth 2.0 Authentication
OAuth 2.0 Flow Overview
Catalio uses Auth0 as the OAuth 2.0 provider with the authorization code flow:
1. User clicks "Sign In" in your application
2. Application redirects to Auth0 authorization URL
3. User authenticates with Auth0 (email/password, SSO, etc.)
4. Auth0 redirects back with authorization code
5. Application exchanges code for access token
6. Application uses access token to call Catalio API
7. Catalio validates token and processes request
Step 1: Register Your Application
Prerequisites:
- Auth0 tenant configured (contact Catalio for tenant details)
- Application registered in Auth0
Auth0 Application Configuration:
- Navigate to Auth0 Dashboard → Applications
- Create new application or use existing
- Configure application settings:
- Application Type: Regular Web Application (for server-side apps) or Single Page Application (for client-side apps)
- Allowed Callback URLs: Your application’s callback endpoint
- Allowed Logout URLs: Your application’s logout endpoint
- Allowed Web Origins: Your application’s domain (for SPAs)
Example Configuration:
Name: My Catalio Integration
Type: Regular Web Application
Callback URLs: https://myapp.example.com/auth/callback
Logout URLs: https://myapp.example.com
Step 2: Initiate Authorization Flow
Authorization Request:
Redirect users to Auth0 authorization endpoint:
GET https://{YOUR_AUTH0_DOMAIN}/authorize?
response_type=code&
client_id={YOUR_CLIENT_ID}&
redirect_uri={YOUR_CALLBACK_URL}&
scope=openid profile email&
state={RANDOM_STATE_VALUE}&
audience={CATALIO_API_AUDIENCE}
Parameters:
| Parameter | Required | Description |
|---|---|---|
response_type |
Yes | Must be code for authorization code flow |
client_id |
Yes | Your Auth0 application client ID |
redirect_uri |
Yes | Callback URL registered in Auth0 |
scope |
Yes | OAuth scopes (minimum: openid profile email) |
state |
Yes | Random value to prevent CSRF attacks |
audience |
No | API identifier for Catalio (if using custom API) |
Example JavaScript (Client-Side):
const auth0Domain = 'your-tenant.auth0.com'
const clientId = 'your_client_id'
const redirectUri = 'https://myapp.example.com/auth/callback'
const state = generateRandomString(32) // Implement secure random generation
const authUrl =
`https://${auth0Domain}/authorize?` +
`response_type=code&` +
`client_id=${clientId}&` +
`redirect_uri=${encodeURIComponent(redirectUri)}&` +
`scope=openid%20profile%20email&` +
`state=${state}`
// Store state in session for validation
sessionStorage.setItem('auth_state', state)
// Redirect user to Auth0
window.location.href = authUrl
Step 3: Handle Callback
Callback Request:
After authentication, Auth0 redirects to your callback URL:
GET https://myapp.example.com/auth/callback?
code=AUTHORIZATION_CODE&
state=ORIGINAL_STATE_VALUE
Validate State Parameter:
const receivedState = new URLSearchParams(window.location.search).get('state')
const storedState = sessionStorage.getItem('auth_state')
if (receivedState !== storedState) {
throw new Error('Invalid state parameter - possible CSRF attack')
}
Step 4: Exchange Code for Tokens
Token Request:
POST https://{YOUR_AUTH0_DOMAIN}/oauth/token
Content-Type: application/json
{
"grant_type": "authorization_code",
"client_id": "{YOUR_CLIENT_ID}",
"client_secret": "{YOUR_CLIENT_SECRET}",
"code": "{AUTHORIZATION_CODE}",
"redirect_uri": "{YOUR_CALLBACK_URL}"
}
Token Response:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "v1.MRrT...",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 86400
}
Example Node.js (Server-Side):
const axios = require('axios')
async function exchangeCodeForToken(authorizationCode) {
const tokenEndpoint = `https://${auth0Domain}/oauth/token`
const response = await axios.post(tokenEndpoint, {
grant_type: 'authorization_code',
client_id: process.env.AUTH0_CLIENT_ID,
client_secret: process.env.AUTH0_CLIENT_SECRET,
code: authorizationCode,
redirect_uri: process.env.AUTH0_CALLBACK_URL,
})
return response.data
}
Example Python:
import requests
import os
def exchange_code_for_token(authorization_code: str) -> dict:
"""Exchange authorization code for access token."""
token_endpoint = f"https://{os.getenv('AUTH0_DOMAIN')}/oauth/token"
response = requests.post(token_endpoint, json={
'grant_type': 'authorization_code',
'client_id': os.getenv('AUTH0_CLIENT_ID'),
'client_secret': os.getenv('AUTH0_CLIENT_SECRET'),
'code': authorization_code,
'redirect_uri': os.getenv('AUTH0_CALLBACK_URL')
})
response.raise_for_status()
return response.json()
Step 5: Refresh Tokens
Refresh Token Flow:
Access tokens expire after 24 hours. Use refresh tokens to obtain new access tokens without re-authentication:
POST https://{YOUR_AUTH0_DOMAIN}/oauth/token
Content-Type: application/json
{
"grant_type": "refresh_token",
"client_id": "{YOUR_CLIENT_ID}",
"client_secret": "{YOUR_CLIENT_SECRET}",
"refresh_token": "{REFRESH_TOKEN}"
}
Refresh Response:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 86400
}
Example Token Refresh (Node.js):
async function refreshAccessToken(refreshToken) {
const tokenEndpoint = `https://${auth0Domain}/oauth/token`
try {
const response = await axios.post(tokenEndpoint, {
grant_type: 'refresh_token',
client_id: process.env.AUTH0_CLIENT_ID,
client_secret: process.env.AUTH0_CLIENT_SECRET,
refresh_token: refreshToken,
})
return response.data.access_token
} catch (error) {
// Refresh token invalid or expired - re-authenticate user
throw new Error('Refresh token expired - please sign in again')
}
}
OAuth 2.0 Best Practices
Security:
- ✅ Always use HTTPS for all OAuth flows
- ✅ Validate
stateparameter to prevent CSRF attacks - ✅ Store client secrets securely (environment variables, secret managers)
- ✅ Never expose client secrets in client-side code
- ✅ Implement PKCE (Proof Key for Code Exchange) for mobile apps and SPAs
Token Management:
- ✅ Store tokens securely (HttpOnly cookies for web apps, secure storage for mobile)
- ✅ Never store tokens in localStorage (vulnerable to XSS)
- ✅ Implement automatic token refresh before expiration
- ✅ Clear tokens on logout
- ✅ Handle token refresh failures gracefully (redirect to login)
User Experience:
- ✅ Preserve redirect destination after authentication
- ✅ Show loading states during OAuth redirects
- ✅ Handle authentication errors with clear messaging
- ✅ Implement “Remember me” via refresh tokens (if appropriate)
API Key Authentication
Overview
API keys provide long-lived authentication for programmatic access. Ideal for:
- CI/CD Pipelines: Automated testing and deployment
- MCP Clients: Claude Desktop and other Model Context Protocol clients
- Background Jobs: Scheduled data sync and automation
- Server-to-Server: Integration between systems
- Developer Tools: CLI tools and scripts
Creating API Keys
Step 1: Navigate to API Keys Settings
- Log into Catalio web application
- Navigate to Settings → API Keys
- Click Generate New API Key
Step 2: Configure API Key
Required Fields:
- Name: Descriptive identifier (e.g., “CI/CD Pipeline - Production”)
- Expiration: Key lifetime (30, 60, 90, or 365 days)
Optional Fields:
- Description: Additional context about key usage
- IP Restrictions: Limit key usage to specific IP addresses (recommended for production)
Example Configuration:
Name: Claude Desktop - MacBook Pro
Expiration: 90 days
Description: MCP client for local development
IP Restrictions: None (development environment)
Step 3: Save API Key
Critical: The plaintext API key is shown only once at creation. Copy immediately and store securely.
Example API Key:
catalio_AKC7J8K9L0M1N2O3P4Q5R6S7T8U9V0W1X2Y3Z4A5B6C7D8E9F0
API Key Format:
- Prefix:
catalio_(identifies provider) - Length: 64 characters (cryptographically secure)
- Storage: SHA-256 hash stored in database (plaintext never persisted)
Using API Keys
Authentication Header:
GET /api/v1/requirements HTTP/1.1
Host: api.catalio.com
Authorization: Bearer catalio_AKC7J8K9L0M1N2O3P4Q5R6S7T8U9V0W1X2Y3Z4A5B6C7D8E9F0
Content-Type: application/json
Example cURL:
curl -X GET /api/v1/requirements \
-H "Authorization: Bearer catalio_AKC7J8K9L0M1N2O3P4Q5R6S7T8U9V0W1X2Y3Z4A5B6C7D8E9F0" \
-H "Content-Type: application/json"
Example Python:
import requests
import os
API_KEY = os.getenv('CATALIO_API_KEY')
BASE_URL = '/api/v1'
def get_requirements():
"""Fetch requirements using API key authentication."""
headers = {
'Authorization': f'Bearer {API_KEY}',
'Content-Type': 'application/json'
}
response = requests.get(f'{BASE_URL}/requirements', headers=headers)
response.raise_for_status()
return response.json()
# Usage
requirements = get_requirements()
Example JavaScript (Node.js):
const axios = require('axios')
const API_KEY = process.env.CATALIO_API_KEY
const BASE_URL = 'https://your-instance.catalio.com/api/v1'
async function getRequirements() {
const response = await axios.get(`${BASE_URL}/requirements`, {
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
})
return response.data
}
// Usage
getRequirements()
.then((requirements) => console.log(requirements))
.catch((error) => console.error('API Error:', error.response?.data))
Example Elixir:
defmodule MyCatalioClient do
@moduledoc "Catalio API client with API key authentication"
@base_url "/api/v1"
def get_requirements do
api_key = System.get_env("CATALIO_API_KEY")
Req.get!("#{@base_url}/requirements",
headers: [
{"Authorization", "Bearer #{api_key}"},
{"Content-Type", "application/json"}
]
)
end
def create_requirement(attrs) do
api_key = System.get_env("CATALIO_API_KEY")
Req.post!("#{@base_url}/requirements",
headers: [
{"Authorization", "Bearer #{api_key}"},
{"Content-Type", "application/json"}
],
json: attrs
)
end
end
API Key Management
Listing Keys:
View all active API keys for your account:
- Navigate to Settings → API Keys
- View table showing:
- Name: Key identifier
- Created: Creation timestamp
- Last Used: Most recent usage timestamp
- Expires: Expiration date
- Actions: Revoke option
Revoking Keys:
Immediately invalidate an API key:
- Navigate to Settings → API Keys
- Click Revoke next to key
- Confirm revocation
Effects of Revocation:
- ✅ Key invalidated immediately
- ✅ All subsequent requests with key return
401 Unauthorized - ✅ Key cannot be restored (generate new key if needed)
- ✅ Soft delete (key remains in audit logs)
Key Rotation:
Best practice: Rotate API keys regularly.
Rotation Process:
- Generate new API key
- Update applications to use new key
- Test new key thoroughly
- Revoke old key after verification period (24-48 hours)
- Document rotation in audit log
Recommended Rotation Schedule:
- Production: Every 90 days
- Development: Every 180 days
- Compromised: Immediately
API Key Security
Storage Best Practices:
✅ DO:
- Store in environment variables
- Use secret management services (AWS Secrets Manager, Azure Key Vault, HashiCorp Vault)
- Encrypt at rest in configuration management tools
- Use CI/CD secret injection (GitHub Secrets, GitLab CI Variables)
- Restrict file permissions on credential files (chmod 600)
❌ DON’T:
- Commit to version control
- Store in plaintext configuration files
- Hardcode in application source code
- Share via email or chat
- Log in application logs
Environment Variable Example:
# .env (NEVER commit this file)
CATALIO_API_KEY=catalio_AKC7J8K9L0M1N2O3P4Q5R6S7T8U9V0W1X2Y3Z4A5B6C7D8E9F0
Git Ignore:
# .gitignore
.env
.env.local
.env.*.local
config/*.secret.exs
secrets/
Docker Secrets:
# docker-compose.yml
version: '3.8'
services:
app:
image: my-catalio-integration:latest
environment:
CATALIO_API_KEY: ${CATALIO_API_KEY}
secrets:
- catalio_api_key
secrets:
catalio_api_key:
external: true
Kubernetes Secrets:
apiVersion: v1
kind: Secret
metadata:
name: catalio-api-key
type: Opaque
stringData:
api-key: catalio_AKC7J8K9L0M1N2O3P4Q5R6S7T8U9V0W1X2Y3Z4A5B6C7D8E9F0
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
template:
spec:
containers:
- name: app
env:
- name: CATALIO_API_KEY
valueFrom:
secretKeyRef:
name: catalio-api-key
key: api-key
JWT Token Structure
Token Anatomy
Catalio uses JSON Web Tokens (JWT) for session management. Tokens are issued after successful OAuth or API key authentication.
JWT Format:
{HEADER}.{PAYLOAD}.{SIGNATURE}
Example JWT:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMzQ1Njc4OTAiLCJvcmciOiJvcmdfYWJjZGVmZ2hpamsiLCJyb2xlIjoiZWRpdG9yIiwiZXhwIjoxNzA0MDY3MjAwLCJpYXQiOjE3MDQwNjM2MDAsImp0aSI6InRva2VuXzk4NzY1NDMyMTAifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Token Claims
Header:
{
"alg": "RS256",
"typ": "JWT"
}
| Claim | Description |
|---|---|
alg |
Signature algorithm (RS256 - RSA with SHA-256) |
typ |
Token type (always JWT) |
Payload:
{
"sub": "user_1234567890",
"org": "org_abcdefghijk",
"role": "editor",
"exp": 1704067200,
"iat": 1704063600,
"jti": "token_9876543210"
}
| Claim | Type | Description |
|---|---|---|
sub |
string | Subject - User ID |
org |
string | Organization ID (tenant identifier) |
role |
string | User role (admin, editor, contributor, viewer) |
exp |
integer | Expiration timestamp (Unix epoch) |
iat |
integer | Issued at timestamp (Unix epoch) |
jti |
string | JWT ID (unique token identifier) |
Signature:
RSA-SHA256 signature ensuring token integrity:
RSASHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
privateKey
)
Token Validation
Client-Side Validation:
function isTokenExpired(token) {
try {
// Decode payload (base64 URL decode)
const payload = JSON.parse(atob(token.split('.')[1]))
// Check expiration
const now = Math.floor(Date.now() / 1000)
return payload.exp < now
} catch (error) {
return true // Invalid token format
}
}
// Usage
if (isTokenExpired(accessToken)) {
// Refresh token or re-authenticate
await refreshAccessToken()
}
Server-Side Validation (Node.js):
const jwt = require('jsonwebtoken')
const jwksClient = require('jwks-rsa')
const client = jwksClient({
jwksUri: 'https://your-tenant.auth0.com/.well-known/jwks.json',
})
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
const signingKey = key.publicKey || key.rsaPublicKey
callback(null, signingKey)
})
}
function validateToken(token) {
return new Promise((resolve, reject) => {
jwt.verify(
token,
getKey,
{
audience: process.env.AUTH0_AUDIENCE,
issuer: `https://${process.env.AUTH0_DOMAIN}/`,
algorithms: ['RS256'],
},
(err, decoded) => {
if (err) {
reject(err)
} else {
resolve(decoded)
}
}
)
})
}
Server-Side Validation (Python):
from jose import jwt
import requests
def get_jwks():
"""Fetch JSON Web Key Set from Auth0."""
jwks_url = f"https://{os.getenv('AUTH0_DOMAIN')}/.well-known/jwks.json"
response = requests.get(jwks_url)
return response.json()
def validate_token(token: str) -> dict:
"""Validate JWT token and return claims."""
jwks = get_jwks()
# Get unverified header to extract kid
unverified_header = jwt.get_unverified_header(token)
# Find matching key in JWKS
rsa_key = {}
for key in jwks['keys']:
if key['kid'] == unverified_header['kid']:
rsa_key = {
'kty': key['kty'],
'kid': key['kid'],
'use': key['use'],
'n': key['n'],
'e': key['e']
}
if not rsa_key:
raise ValueError('Unable to find appropriate key')
# Validate token
payload = jwt.decode(
token,
rsa_key,
algorithms=['RS256'],
audience=os.getenv('AUTH0_AUDIENCE'),
issuer=f"https://{os.getenv('AUTH0_DOMAIN')}/"
)
return payload
Token Lifecycle
Issuance:
- User authenticates via OAuth or API key
- Catalio generates JWT with user claims
- Token signed with private key
- Token returned to client
Usage:
- Client includes token in
Authorizationheader - Catalio validates signature and expiration
- Catalio extracts user ID and organization
- Request processed with actor context
Expiration:
- Access Tokens: 24 hours
- Refresh Tokens: 30 days (rolling window)
Renewal:
- Use refresh token to obtain new access token
- No re-authentication required (unless refresh token expired)
Revocation:
- Logout: Token removed from session store
- API Key Revocation: All associated tokens invalidated
- User Suspension: All user tokens invalidated
Multi-Tenant Authorization
Tenant Isolation
Catalio is multi-tenant by design. Every API request is automatically scoped to the user’s organization:
Organization Extraction:
User authenticates → JWT issued with `org` claim →
All queries filtered by organization_id → Data isolation guaranteed
Example Query (Elixir):
# User's organization extracted from JWT token
organization_id = actor.organization_id
# All reads automatically scoped to organization
Requirements
|> Ash.Query.for_read(:read, actor: user, tenant: organization_id)
|> Ash.read!()
# Results only include requirements belonging to user's organization
Organization Scoping
How It Works:
- Authentication: JWT contains
orgclaim identifying user’s organization - Actor Context: Organization ID extracted and set as tenant
- Query Filtering: All database queries automatically filtered by
organization_id - Policy Enforcement: Ash policies ensure cross-tenant access is denied
Example API Request:
GET /api/v1/requirements HTTP/1.1
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Token Payload:
{
"sub": "user_1234567890",
"org": "org_acme_corp",
"role": "editor"
}
Backend Processing:
# Extract organization from token
tenant = token_claims["org"] # "org_acme_corp"
# Query requirements with tenant context
Requirements
|> Ash.Query.for_read(:read, actor: user, tenant: tenant)
|> Ash.read!()
# SQL generated:
# SELECT * FROM requirements WHERE organization_id = 'org_acme_corp'
Result:
{
"data": [
{
"id": "req_12345",
"title": "User Authentication",
"organization_id": "org_acme_corp",
...
}
]
}
Cross-Tenant Access Prevention
Policy Enforcement:
Ash policies prevent accessing resources from other organizations:
# Requirement resource policy
policy action_type([:read, :update, :destroy]) do
authorize_if expr(organization_id == ^actor(:organization_id))
end
Attempted Cross-Tenant Access:
GET /api/v1/requirements/req_from_other_org HTTP/1.1
Authorization: Bearer {token_with_org_acme_corp}
Response:
{
"error": "forbidden",
"message": "You are not authorized to perform this action",
"status": 403
}
Multi-Organization Users
If a user belongs to multiple organizations (future feature), API requests must specify the target organization:
Header-Based Organization Selection:
GET /api/v1/requirements HTTP/1.1
Authorization: Bearer {access_token}
X-Catalio-Organization: org_acme_corp
Query Parameter Alternative:
GET /api/v1/requirements?organization_id=org_acme_corp HTTP/1.1
Authorization: Bearer {access_token}
Validation:
- ✅ User must belong to requested organization
- ✅ Organization ID must match JWT claim or be in user’s allowed organizations
- ❌ Attempting to access unauthorized organization returns
403 Forbidden
Role-Based Access Control
Role Hierarchy
Catalio uses four primary roles:
| Role | Permissions | Use Cases |
|---|---|---|
| Viewer | Read-only access | Stakeholders, auditors, external reviewers |
| Contributor | Create and edit own resources | Team members, developers, analysts |
| Editor | Create and edit all resources | Project managers, lead analysts |
| Admin | Full access including settings | Organization administrators, IT leads |
Permission Matrix
| Action | Viewer | Contributor | Editor | Admin |
|---|---|---|---|---|
| View requirements | ✅ | ✅ | ✅ | ✅ |
| Create requirements | ❌ | ✅ | ✅ | ✅ |
| Edit own requirements | ❌ | ✅ | ✅ | ✅ |
| Edit all requirements | ❌ | ❌ | ✅ | ✅ |
| Delete requirements | ❌ | ❌ | ✅ | ✅ |
| Approve requirements | ❌ | ❌ | ✅ | ✅ |
| View users | ✅ | ✅ | ✅ | ✅ |
| Invite users | ❌ | ❌ | ❌ | ✅ |
| Manage roles | ❌ | ❌ | ❌ | ✅ |
| Suspend users | ❌ | ❌ | ❌ | ✅ |
| View settings | ❌ | ❌ | ❌ | ✅ |
| Configure integrations | ❌ | ❌ | ❌ | ✅ |
| Manage API keys | ❌ | User’s own | User’s own | All |
| Billing access | ❌ | ❌ | ❌ | ✅ |
Checking Permissions (API)
Endpoint:
GET /api/v1/users/me/permissions HTTP/1.1
Authorization: Bearer {access_token}
Response:
{
"user_id": "user_1234567890",
"organization_id": "org_acme_corp",
"role": "editor",
"permissions": {
"requirements": {
"read": true,
"create": true,
"update": true,
"delete": true,
"approve": true
},
"users": {
"read": true,
"invite": false,
"manage_roles": false
},
"settings": {
"read": false,
"update": false
},
"api_keys": {
"create": true,
"read_own": true,
"read_all": false,
"revoke_own": true,
"revoke_all": false
}
}
}
Role Assignment
User Role:
User roles are assigned during registration and stored in Auth0:
Auth0 User Metadata:
{
"user_id": "auth0|1234567890",
"email": "user@example.com",
"app_metadata": {
"organization_id": "org_acme_corp",
"role": "editor"
}
}
JWT Token Claims:
{
"sub": "auth0|1234567890",
"org": "org_acme_corp",
"role": "editor"
}
Changing User Roles:
Only admins can change user roles:
PATCH /api/v1/users/{user_id} HTTP/1.1
Authorization: Bearer {admin_access_token}
Content-Type: application/json
{
"role": "contributor"
}
Response:
{
"id": "user_1234567890",
"email": "user@example.com",
"role": "contributor",
"updated_at": "2025-03-01T14:30:00Z"
}
Making Authenticated Requests
Request Structure
Standard API Request:
{METHOD} /api/v1/{resource} HTTP/1.1
Host: api.catalio.com
Authorization: Bearer {access_token_or_api_key}
Content-Type: application/json
Accept: application/json
Headers
Required Headers:
| Header | Value | Description |
|---|---|---|
Authorization |
Bearer {token} |
JWT access token or API key |
Content-Type |
application/json |
Request body format (for POST/PATCH) |
Accept |
application/json |
Response format |
Optional Headers:
| Header | Value | Description |
|---|---|---|
X-Request-ID |
UUID | Request tracing (auto-generated if omitted) |
X-Catalio-Organization |
Organization ID | Multi-org users only (future feature) |
Examples by Language
cURL:
#!/bin/bash
# Set variables
API_KEY="${CATALIO_API_KEY}" # Set via environment variable
BASE_URL="https://your-instance.catalio.com/api/v1"
# GET request
curl -X GET "${BASE_URL}/requirements" \
-H "Authorization: Bearer ${API_KEY}" \
-H "Content-Type: application/json"
# POST request
curl -X POST "${BASE_URL}/requirements" \
-H "Authorization: Bearer ${API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"title": "User Authentication",
"description": "As a user, I want to log in with email and password",
"priority": "high",
"status": "draft"
}'
# PATCH request
curl -X PATCH "${BASE_URL}/requirements/req_12345" \
-H "Authorization: Bearer ${API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"status": "approved"
}'
# DELETE request
curl -X DELETE "${BASE_URL}/requirements/req_12345" \
-H "Authorization: Bearer ${API_KEY}"
Python:
import requests
import os
from typing import Optional, Dict, Any
class CatalioClient:
"""Catalio API client with authentication."""
def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None):
self.api_key = api_key or os.getenv('CATALIO_API_KEY')
self.base_url = base_url or os.getenv('CATALIO_BASE_URL', 'https://your-instance.catalio.com/api/v1')
self.session = requests.Session()
self.session.headers.update({
'Authorization': f'Bearer {self.api_key}',
'Content-Type': 'application/json',
'Accept': 'application/json'
})
def get_requirements(self, filters: Optional[Dict[str, Any]] = None) -> list:
"""Fetch requirements with optional filters."""
response = self.session.get(
f'{self.base_url}/requirements',
params=filters
)
response.raise_for_status()
return response.json()['data']
def create_requirement(self, data: Dict[str, Any]) -> dict:
"""Create a new requirement."""
response = self.session.post(
f'{self.base_url}/requirements',
json=data
)
response.raise_for_status()
return response.json()
def update_requirement(self, requirement_id: str, data: Dict[str, Any]) -> dict:
"""Update an existing requirement."""
response = self.session.patch(
f'{self.base_url}/requirements/{requirement_id}',
json=data
)
response.raise_for_status()
return response.json()
def delete_requirement(self, requirement_id: str) -> None:
"""Delete a requirement."""
response = self.session.delete(
f'{self.base_url}/requirements/{requirement_id}'
)
response.raise_for_status()
# Usage
client = CatalioClient()
# List requirements
requirements = client.get_requirements(filters={'status': 'approved'})
# Create requirement
new_req = client.create_requirement({
'title': 'User Authentication',
'description': 'As a user, I want to log in with email and password',
'priority': 'high',
'status': 'draft'
})
# Update requirement
updated_req = client.update_requirement(new_req['id'], {'status': 'approved'})
# Delete requirement
client.delete_requirement(new_req['id'])
JavaScript (Node.js):
const axios = require('axios')
class CatalioClient {
constructor(apiKey = process.env.CATALIO_API_KEY, baseURL = process.env.CATALIO_BASE_URL) {
this.apiKey = apiKey
this.baseURL = baseURL || 'https://your-instance.catalio.com/api/v1'
this.client = axios.create({
baseURL: this.baseURL,
headers: {
Authorization: `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
Accept: 'application/json',
},
})
// Add response interceptor for error handling
this.client.interceptors.response.use(
(response) => response.data,
(error) => {
if (error.response) {
// API returned error response
throw new Error(`API Error: ${error.response.data.message}`)
} else if (error.request) {
// Network error
throw new Error('Network Error: Unable to reach Catalio API')
} else {
throw error
}
}
)
}
async getRequirements(filters = {}) {
const response = await this.client.get('/requirements', { params: filters })
return response.data
}
async createRequirement(data) {
return await this.client.post('/requirements', data)
}
async updateRequirement(requirementId, data) {
return await this.client.patch(`/requirements/${requirementId}`, data)
}
async deleteRequirement(requirementId) {
return await this.client.delete(`/requirements/${requirementId}`)
}
}
// Usage
const client = new CatalioClient()
async function main() {
try {
// List requirements
const requirements = await client.getRequirements({ status: 'approved' })
console.log(`Found ${requirements.length} approved requirements`)
// Create requirement
const newReq = await client.createRequirement({
title: 'User Authentication',
description: 'As a user, I want to log in with email and password',
priority: 'high',
status: 'draft',
})
console.log('Created requirement:', newReq.id)
// Update requirement
const updatedReq = await client.updateRequirement(newReq.id, {
status: 'approved',
})
console.log('Updated requirement status:', updatedReq.status)
// Delete requirement
await client.deleteRequirement(newReq.id)
console.log('Deleted requirement:', newReq.id)
} catch (error) {
console.error('Error:', error.message)
}
}
main()
Elixir:
defmodule CatalioClient do
@moduledoc """
Catalio API client with authentication support.
## Usage
# List requirements
{:ok, requirements} = CatalioClient.get_requirements()
# Create requirement
{:ok, requirement} = CatalioClient.create_requirement(%{
title: "User Authentication",
description: "As a user, I want to log in",
priority: "high",
status: "draft"
})
# Update requirement
{:ok, updated} = CatalioClient.update_requirement(requirement.id, %{
status: "approved"
})
# Delete requirement
:ok = CatalioClient.delete_requirement(requirement.id)
"""
@base_url "/api/v1"
defp api_key do
System.get_env("CATALIO_API_KEY") ||
raise "CATALIO_API_KEY environment variable not set"
end
defp headers do
[
{"Authorization", "Bearer #{api_key()}"},
{"Content-Type", "application/json"},
{"Accept", "application/json"}
]
end
@doc "Fetch requirements with optional filters"
def get_requirements(filters \\ %{}) do
url = "#{@base_url}/requirements"
case Req.get(url, headers: headers(), params: filters) do
{:ok, %{status: 200, body: body}} ->
{:ok, body["data"]}
{:ok, %{status: status, body: body}} ->
{:error, "API error #{status}: #{body["message"]}"}
{:error, reason} ->
{:error, "Request failed: #{inspect(reason)}"}
end
end
@doc "Create a new requirement"
def create_requirement(attrs) do
url = "#{@base_url}/requirements"
case Req.post(url, headers: headers(), json: attrs) do
{:ok, %{status: 201, body: body}} ->
{:ok, body}
{:ok, %{status: status, body: body}} ->
{:error, "API error #{status}: #{body["message"]}"}
{:error, reason} ->
{:error, "Request failed: #{inspect(reason)}"}
end
end
@doc "Update an existing requirement"
def update_requirement(requirement_id, attrs) do
url = "#{@base_url}/requirements/#{requirement_id}"
case Req.patch(url, headers: headers(), json: attrs) do
{:ok, %{status: 200, body: body}} ->
{:ok, body}
{:ok, %{status: status, body: body}} ->
{:error, "API error #{status}: #{body["message"]}"}
{:error, reason} ->
{:error, "Request failed: #{inspect(reason)}"}
end
end
@doc "Delete a requirement"
def delete_requirement(requirement_id) do
url = "#{@base_url}/requirements/#{requirement_id}"
case Req.delete(url, headers: headers()) do
{:ok, %{status: 204}} ->
:ok
{:ok, %{status: status, body: body}} ->
{:error, "API error #{status}: #{body["message"]}"}
{:error, reason} ->
{:error, "Request failed: #{inspect(reason)}"}
end
end
end
Security Best Practices
Credential Management
Environment Variables:
# .env (development)
CATALIO_API_KEY=catalio_AKC7J8K9L0M1N2O3P4Q5R6S7T8U9V0W1X2Y3Z4A5B6C7D8E9F0
AUTH0_CLIENT_ID=your_client_id
AUTH0_CLIENT_SECRET=your_client_secret
AUTH0_DOMAIN=your-tenant.auth0.com
Secret Management Services:
AWS Secrets Manager:
# Store secret
aws secretsmanager create-secret \
--name catalio/api-key \
--secret-string "catalio_AKC7J8K9..."
# Retrieve secret
aws secretsmanager get-secret-value \
--secret-id catalio/api-key \
--query SecretString \
--output text
Azure Key Vault:
# Store secret
az keyvault secret set \
--vault-name my-key-vault \
--name catalio-api-key \
--value "catalio_AKC7J8K9..."
# Retrieve secret
az keyvault secret show \
--vault-name my-key-vault \
--name catalio-api-key \
--query value \
--output tsv
HashiCorp Vault:
# Store secret
vault kv put secret/catalio api_key="catalio_AKC7J8K9..."
# Retrieve secret
vault kv get -field=api_key secret/catalio
HTTPS Enforcement
Always use HTTPS:
- ✅ All API requests use HTTPS protocol
- ❌ Never use
http://(requests will be rejected) - ✅ Verify SSL certificates
- ✅ Use modern TLS versions (TLS 1.2+)
Certificate Verification (Python):
import requests
# ✅ Correct - verify SSL certificates
response = requests.get('/api/v1/requirements', verify=True)
# ❌ NEVER do this in production
response = requests.get('/api/v1/requirements', verify=False)
Token Storage
Web Applications:
✅ Secure Storage:
- HttpOnly cookies (not accessible via JavaScript)
- Session storage with HTTPS-only flag
- Secure, SameSite cookie attributes
❌ Insecure Storage:
- localStorage (vulnerable to XSS)
- sessionStorage (vulnerable to XSS)
- Cookies without Secure/HttpOnly flags
Example Secure Cookie (Express.js):
app.post('/auth/callback', async (req, res) => {
const { accessToken, refreshToken } = await exchangeCodeForTokens(req.body.code)
// Store access token in HttpOnly cookie
res.cookie('access_token', accessToken, {
httpOnly: true,
secure: true, // HTTPS only
sameSite: 'strict',
maxAge: 24 * 60 * 60 * 1000, // 24 hours
})
// Store refresh token in HttpOnly cookie
res.cookie('refresh_token', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
})
res.redirect('/dashboard')
})
Mobile Applications:
- Use platform-specific secure storage
- iOS: Keychain Services
- Android: Android Keystore System
- Never store tokens in shared preferences or user defaults
Request Signing
For high-security integrations, implement request signing:
HMAC Request Signature:
import hmac
import hashlib
import base64
from datetime import datetime
def sign_request(api_key: str, secret: str, method: str, path: str, body: str = '') -> str:
"""Generate HMAC signature for API request."""
timestamp = datetime.utcnow().isoformat()
# Create signature payload
payload = f"{method}\n{path}\n{timestamp}\n{body}"
# Generate HMAC signature
signature = hmac.new(
secret.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).digest()
# Base64 encode
signature_b64 = base64.b64encode(signature).decode('utf-8')
return f"{api_key}:{timestamp}:{signature_b64}"
# Usage
api_key = "catalio_AKC7J8K9..."
secret = "your_signing_secret"
signature = sign_request(api_key, secret, "POST", "/api/v1/requirements", '{"title":"Test"}')
headers = {
'Authorization': f'Bearer {api_key}',
'X-Catalio-Signature': signature
}
Rate Limiting
Rate Limit Tiers
Catalio enforces rate limits to ensure API stability and fair usage:
| Authentication | Requests per Minute | Requests per Hour | Requests per Day |
|---|---|---|---|
| Unauthenticated | 10 | 60 | 500 |
| API Key (Free) | 60 | 1,000 | 10,000 |
| API Key (Pro) | 120 | 5,000 | 100,000 |
| API Key (Enterprise) | 300 | 20,000 | 500,000 |
Rate Limit Headers
Every API response includes rate limit information:
HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 987
X-RateLimit-Reset: 1704067200
X-RateLimit-Window: 3600
| Header | Description |
|---|---|
X-RateLimit-Limit |
Maximum requests allowed in current window |
X-RateLimit-Remaining |
Requests remaining in current window |
X-RateLimit-Reset |
Unix timestamp when limit resets |
X-RateLimit-Window |
Rate limit window in seconds |
Handling Rate Limits
Example: Retry with Backoff (Python):
import time
import requests
def make_api_request_with_retry(url: str, headers: dict, max_retries: int = 3) -> dict:
"""Make API request with exponential backoff on rate limit."""
for attempt in range(max_retries):
response = requests.get(url, headers=headers)
if response.status_code == 200:
return response.json()
elif response.status_code == 429: # Rate limit exceeded
# Get retry-after header or calculate from reset time
retry_after = int(response.headers.get('Retry-After', 60))
if attempt < max_retries - 1:
print(f"Rate limited. Retrying in {retry_after} seconds...")
time.sleep(retry_after)
else:
raise Exception('Rate limit exceeded after max retries')
else:
response.raise_for_status()
raise Exception('Max retries exceeded')
Example: Pre-emptive Rate Limit Checking (JavaScript):
class RateLimitedClient {
constructor(apiKey) {
this.apiKey = apiKey
this.remaining = null
this.resetTime = null
}
async makeRequest(url) {
// Check if we should wait for rate limit reset
if (this.remaining !== null && this.remaining <= 0) {
const now = Date.now() / 1000
if (now < this.resetTime) {
const waitTime = (this.resetTime - now) * 1000
console.log(`Rate limit exhausted. Waiting ${waitTime}ms...`)
await new Promise((resolve) => setTimeout(resolve, waitTime))
}
}
// Make request
const response = await axios.get(url, {
headers: { Authorization: `Bearer ${this.apiKey}` },
})
// Update rate limit tracking
this.remaining = parseInt(response.headers['x-ratelimit-remaining'])
this.resetTime = parseInt(response.headers['x-ratelimit-reset'])
return response.data
}
}
Best Practices
Optimize Request Volume:
- ✅ Use bulk endpoints when available
- ✅ Cache responses to reduce redundant requests
- ✅ Use webhooks for real-time updates instead of polling
- ✅ Implement request queuing for high-volume operations
Monitor Usage:
- ✅ Track rate limit headers in application logs
- ✅ Set up alerts for approaching rate limits
- ✅ Monitor trends to predict capacity needs
Upgrade Tier:
- If consistently hitting rate limits, consider upgrading to higher tier
- Enterprise plans offer custom rate limits
Common Authentication Errors
Error Reference
401 Unauthorized:
{
"error": "unauthorized",
"message": "Invalid or missing authentication token",
"status": 401
}
Causes:
- Missing
Authorizationheader - Invalid API key or access token
- Expired access token
- Malformed Bearer token format
Solutions:
- Verify
Authorization: Bearer {token}header is present - Check API key hasn’t been revoked
- Refresh access token if expired
- Ensure token format is correct
403 Forbidden:
{
"error": "forbidden",
"message": "You are not authorized to perform this action",
"status": 403
}
Causes:
- Insufficient role permissions
- Attempting to access another organization’s resources
- Resource-specific policy denial
Solutions:
- Verify user role has required permissions
- Check resource belongs to user’s organization
- Contact administrator to request elevated permissions
429 Too Many Requests:
{
"error": "rate_limit_exceeded",
"message": "Rate limit exceeded. Retry after 60 seconds.",
"status": 429,
"retry_after": 60
}
Causes:
- Exceeded requests per minute/hour limit
- Burst traffic exceeding allowed rate
Solutions:
- Implement exponential backoff and retry
- Reduce request frequency
- Cache responses to minimize redundant requests
- Upgrade to higher tier for increased limits
422 Unprocessable Entity:
{
"error": "validation_failed",
"message": "Validation failed",
"status": 422,
"errors": [
{
"field": "title",
"message": "Title is required"
}
]
}
Causes:
- Invalid request payload
- Missing required fields
- Field validation errors
Solutions:
- Review API documentation for required fields
- Validate request payload before sending
- Check field constraints (length, format, etc.)
Troubleshooting
Debug Authentication Issues
Enable Debug Logging:
import logging
import requests
# Enable debug logging
logging.basicConfig(level=logging.DEBUG)
# Make request with verbose logging
response = requests.get(
'/api/v1/requirements',
headers={'Authorization': f'Bearer {api_key}'}
)
Inspect JWT Token:
# Decode JWT payload (base64 URL decode)
echo "eyJzdWIiOiJ1c2VyXzEyMzQ1Njc4OTAiLCJvcmciOiJvcmdfYWJjZGVmZ2hpamsiLCJyb2xlIjoiZWRpdG9yIiwiZXhwIjoxNzA0MDY3MjAwLCJpYXQiOjE3MDQwNjM2MDAsImp0aSI6InRva2VuXzk4NzY1NDMyMTAifQ" | base64 -d
Test API Key:
curl -v /api/v1/users/me \
-H "Authorization: Bearer ${CATALIO_API_KEY}"
Common Issues
Issue: “Invalid API Key”
Check:
- ✅ API key hasn’t been revoked
- ✅ No extra whitespace in key
- ✅ Correct environment variable loaded
- ✅ Key belongs to correct organization
Issue: “Token Expired”
Solution:
- Refresh access token using refresh token
- Re-authenticate if refresh token also expired
Issue: “Forbidden - Organization Mismatch”
Check:
- ✅ Resource belongs to user’s organization
- ✅ JWT
orgclaim matches resource organization_id - ✅ User hasn’t switched organizations
Support
Documentation:
Contact:
- Email: security@catalio.com
- Support Portal: support.catalio.com
- Enterprise Support: Available for Enterprise tier customers
Next Steps:
Last Updated: March 1, 2025 Applies to: Catalio API v1.0+, Auth0 OAuth 2.0, JWT RS256