API Guides

API Authentication & Authorization

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

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:

  1. Navigate to Auth0 Dashboard → Applications
  2. Create new application or use existing
  3. 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 state parameter 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

  1. Log into Catalio web application
  2. Navigate to SettingsAPI Keys
  3. 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:

  1. Navigate to SettingsAPI Keys
  2. 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:

  1. Navigate to SettingsAPI Keys
  2. Click Revoke next to key
  3. 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:

  1. Generate new API key
  2. Update applications to use new key
  3. Test new key thoroughly
  4. Revoke old key after verification period (24-48 hours)
  5. 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:

  1. User authenticates via OAuth or API key
  2. Catalio generates JWT with user claims
  3. Token signed with private key
  4. Token returned to client

Usage:

  1. Client includes token in Authorization header
  2. Catalio validates signature and expiration
  3. Catalio extracts user ID and organization
  4. 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:

  1. Authentication: JWT contains org claim identifying user’s organization
  2. Actor Context: Organization ID extracted and set as tenant
  3. Query Filtering: All database queries automatically filtered by organization_id
  4. 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 Authorization header
  • 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 org claim matches resource organization_id
  • ✅ User hasn’t switched organizations

Support

Documentation:

Contact:


Next Steps:


Last Updated: March 1, 2025 Applies to: Catalio API v1.0+, Auth0 OAuth 2.0, JWT RS256