Skip to main content

CODITECT License Management API Documentation

Version: 1.0.0 Base URL: https://api.coditect.com (Production) Base URL: http://localhost:8000 (Development)

Table of Contents


Authentication

This API uses JWT (JSON Web Token) authentication via Firebase.

Authentication Header

Include your Firebase JWT token in all authenticated requests:

Authorization: Bearer <your-jwt-token>

Obtaining a Token

Tokens are obtained through Firebase Authentication. See Firebase documentation for authentication flows.

Token Lifetime:

  • Access Token: 15 minutes
  • Refresh Token: 7 days

Overview

The CODITECT License Management API provides atomic license seat management with the following features:

  • Atomic Seat Counting: Redis-based Lua scripts ensure thread-safe seat allocation
  • Digital Signatures: Cloud KMS RSA-4096 signing for tamper-proof license validation
  • SOC 2 Compliance: Comprehensive audit logging for all operations
  • Multi-Tenant: Organization-based data isolation
  • Real-time Monitoring: 6-minute session timeout with heartbeat mechanism

Session Lifecycle

  1. Acquire a license seat (POST /api/v1/licenses/acquire)
  2. Maintain the session with heartbeats every 3-5 minutes (PATCH /api/v1/licenses/sessions/{id}/heartbeat)
  3. Release the seat when done (DELETE /api/v1/licenses/sessions/{id})

Endpoints

License Acquisition

Atomically acquire a license seat for the authenticated user.

Endpoint: POST /api/v1/licenses/acquire

Authentication: Required (JWT)

Request Body:

{
"license_key": "PROD-2025-ABCD-1234",
"hardware_id": "mac-address-hash-xyz",
"ip_address": "192.168.1.100",
"user_agent": "CoditectClient/1.0.0 (Windows NT 10.0)"
}

Request Fields:

FieldTypeRequiredDescription
license_keystringYesLicense key in format XXXX-XXXX-XXXX-XXXX
hardware_idstringYesUnique hardware identifier (e.g., hashed MAC address)
ip_addressstring (IP)NoClient IP address (auto-detected if not provided)
user_agentstringNoClient user agent string (auto-detected if not provided)

Success Response (201 Created):

{
"id": "a3b2c1d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"organization": "org-uuid-123",
"license": "lic-uuid-456",
"license_key": "PROD-2025-ABCD-1234",
"user": "user-uuid-789",
"user_email": "john.doe@example.com",
"hardware_id": "mac-address-hash-xyz",
"ip_address": "192.168.1.100",
"user_agent": "CoditectClient/1.0.0 (Windows NT 10.0)",
"started_at": "2025-11-30T10:00:00Z",
"last_heartbeat_at": "2025-11-30T10:00:00Z",
"ended_at": null,
"is_active": true,
"duration": 0,
"signed_license": {
"payload": {
"session_id": "a3b2c1d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"license_id": "lic-uuid-456",
"license_key": "PROD-2025-ABCD-1234",
"user_id": "user-uuid-789",
"user_email": "john.doe@example.com",
"organization_id": "org-uuid-123",
"tier": "enterprise",
"features": ["feature1", "feature2"],
"expiry_date": "2026-11-30T00:00:00Z",
"issued_at": "2025-11-30T10:00:00Z"
},
"signature": "base64-encoded-signature-here...",
"algorithm": "RS256",
"key_id": "projects/PROJECT_ID/locations/global/keyRings/RING/cryptoKeys/KEY"
}
}

Existing Session Response (200 OK):

If the user already has an active session on this hardware, the existing session is returned:

{
"id": "existing-session-uuid",
"license_key": "PROD-2025-ABCD-1234",
"user_email": "john.doe@example.com",
"hardware_id": "mac-address-hash-xyz",
"started_at": "2025-11-30T09:30:00Z",
"last_heartbeat_at": "2025-11-30T09:55:00Z",
"is_active": true,
"duration": 1800
}

Error Responses:

Status CodeDescriptionExample Response
400 Bad RequestInvalid request data{"license_key": ["Invalid license key."]}
401 UnauthorizedMissing or invalid authentication{"detail": "Authentication credentials were not provided."}
409 ConflictNo available seats{"error": "No available seats", "detail": "Maximum concurrent seats (10) reached for your organization"}
503 Service UnavailableRedis offline{"error": "License service unavailable (Redis offline)"}
500 Internal Server ErrorServer error{"error": "Failed to acquire license", "detail": "..."}

Session Heartbeat

Update the heartbeat timestamp for an active license session. Must be called at least once every 6 minutes to keep the session active.

Endpoint: PATCH /api/v1/licenses/sessions/{session_id}/heartbeat

Authentication: Required (JWT)

URL Parameters:

ParameterTypeRequiredDescription
session_idUUIDYesUUID of the license session

Request Body: Empty (no body required)

Success Response (200 OK):

{
"id": "a3b2c1d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"last_heartbeat_at": "2025-11-30T10:05:00Z",
"is_active": true
}

Error Responses:

Status CodeDescriptionExample Response
400 Bad RequestSession already ended{"error": "Session already ended"}
401 UnauthorizedMissing or invalid authentication{"detail": "Authentication credentials were not provided."}
404 Not FoundSession not found{"error": "Session not found"}
410 GoneSession expired in Redis{"error": "Session expired or not found in active pool"}
503 Service UnavailableRedis offline{"error": "License service unavailable (Redis offline)"}
500 Internal Server ErrorServer error{"error": "Failed to update heartbeat", "detail": "..."}

Best Practices:

  • Call this endpoint every 3-5 minutes to maintain your license seat
  • Sessions expire after 6 minutes without a heartbeat
  • Implement retry logic with exponential backoff for network failures

License Release

Release a license seat by ending the session. This endpoint is idempotent and can be called multiple times safely.

Endpoint: DELETE /api/v1/licenses/sessions/{session_id}

Authentication: Required (JWT)

URL Parameters:

ParameterTypeRequiredDescription
session_idUUIDYesUUID of the license session to release

Request Body: None

Success Response (200 OK):

{
"message": "License released successfully",
"session_id": "a3b2c1d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"ended_at": "2025-11-30T10:30:00Z"
}

Already Released Response (200 OK):

{
"message": "Session already ended",
"session_id": "a3b2c1d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"ended_at": "2025-11-30T10:25:00Z"
}

Error Responses:

Status CodeDescriptionExample Response
401 UnauthorizedMissing or invalid authentication{"detail": "Authentication credentials were not provided."}
404 Not FoundSession not found{"error": "Session not found"}
503 Service UnavailableRedis offline{"error": "License service unavailable (Redis offline)"}
500 Internal Server ErrorServer error{"error": "Failed to release license", "detail": "..."}

When to Call:

  • User explicitly logs out
  • Application is shutting down
  • User switches to a different license
  • Application crash recovery (cleanup on restart)

Error Responses

All error responses follow a consistent format:

{
"error": "Error type",
"detail": "Detailed error message"
}

Common Error Types

Validation Errors (400):

{
"license_key": ["This field is required."],
"hardware_id": ["Hardware ID is required."]
}

Authentication Errors (401):

{
"detail": "Authentication credentials were not provided."
}
{
"detail": "Given token not valid for any token type"
}

Resource Conflict (409):

{
"error": "No available seats",
"detail": "Maximum concurrent seats (10) reached for your organization"
}

Service Unavailable (503):

{
"error": "License service unavailable (Redis offline)"
}

Rate Limits

Rate limits are enforced per user:

Endpoint CategoryLimit
Authentication5 requests/minute
License Operations100 requests/minute
Heartbeat20 requests/minute

Rate Limit Headers:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1638360000

Rate Limit Exceeded Response (429):

{
"error": "Rate limit exceeded",
"detail": "Too many requests. Please try again in 30 seconds."
}

Examples

Complete Session Lifecycle

1. Acquire License

curl -X POST https://api.coditect.com/api/v1/licenses/acquire \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"license_key": "PROD-2025-ABCD-1234",
"hardware_id": "mac-address-hash-xyz"
}'

Response:

{
"id": "session-uuid-123",
"license_key": "PROD-2025-ABCD-1234",
"user_email": "john.doe@example.com",
"started_at": "2025-11-30T10:00:00Z",
"is_active": true,
"signed_license": { ... }
}

2. Send Heartbeat (every 3-5 minutes)

curl -X PATCH https://api.coditect.com/api/v1/licenses/sessions/session-uuid-123/heartbeat \
-H "Authorization: Bearer YOUR_JWT_TOKEN"

Response:

{
"id": "session-uuid-123",
"last_heartbeat_at": "2025-11-30T10:05:00Z",
"is_active": true
}

3. Release License

curl -X DELETE https://api.coditect.com/api/v1/licenses/sessions/session-uuid-123 \
-H "Authorization: Bearer YOUR_JWT_TOKEN"

Response:

{
"message": "License released successfully",
"session_id": "session-uuid-123",
"ended_at": "2025-11-30T10:30:00Z"
}

Python Client Example

import requests
from typing import Optional

class CoditectLicenseClient:
"""Client for CODITECT License Management API."""

def __init__(self, base_url: str, jwt_token: str):
self.base_url = base_url
self.headers = {
'Authorization': f'Bearer {jwt_token}',
'Content-Type': 'application/json'
}
self.session_id: Optional[str] = None

def acquire_license(self, license_key: str, hardware_id: str) -> dict:
"""Acquire a license seat."""
url = f"{self.base_url}/api/v1/licenses/acquire"
data = {
"license_key": license_key,
"hardware_id": hardware_id
}

response = requests.post(url, json=data, headers=self.headers)
response.raise_for_status()

session_data = response.json()
self.session_id = session_data['id']
return session_data

def heartbeat(self) -> dict:
"""Send heartbeat to maintain session."""
if not self.session_id:
raise ValueError("No active session")

url = f"{self.base_url}/api/v1/licenses/sessions/{self.session_id}/heartbeat"
response = requests.patch(url, headers=self.headers)
response.raise_for_status()

return response.json()

def release_license(self) -> dict:
"""Release the license seat."""
if not self.session_id:
raise ValueError("No active session")

url = f"{self.base_url}/api/v1/licenses/sessions/{self.session_id}"
response = requests.delete(url, headers=self.headers)
response.raise_for_status()

result = response.json()
self.session_id = None
return result

# Usage
client = CoditectLicenseClient(
base_url="https://api.coditect.com",
jwt_token="your-firebase-jwt-token"
)

# Acquire license
session = client.acquire_license(
license_key="PROD-2025-ABCD-1234",
hardware_id="mac-address-hash-xyz"
)
print(f"Session acquired: {session['id']}")

# Maintain session with heartbeats (run in background thread)
import time
import threading

def heartbeat_loop():
while client.session_id:
try:
client.heartbeat()
print("Heartbeat sent")
except Exception as e:
print(f"Heartbeat failed: {e}")
time.sleep(180) # 3 minutes

heartbeat_thread = threading.Thread(target=heartbeat_loop, daemon=True)
heartbeat_thread.start()

# ... do work ...

# Release when done
client.release_license()
print("License released")

JavaScript Client Example

class CoditectLicenseClient {
constructor(baseUrl, jwtToken) {
this.baseUrl = baseUrl;
this.jwtToken = jwtToken;
this.sessionId = null;
this.heartbeatInterval = null;
}

async acquireLicense(licenseKey, hardwareId) {
const response = await fetch(`${this.baseUrl}/api/v1/licenses/acquire`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.jwtToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
license_key: licenseKey,
hardware_id: hardwareId
})
});

if (!response.ok) {
throw new Error(`Acquisition failed: ${response.statusText}`);
}

const session = await response.json();
this.sessionId = session.id;
this.startHeartbeat();
return session;
}

async heartbeat() {
if (!this.sessionId) {
throw new Error('No active session');
}

const response = await fetch(
`${this.baseUrl}/api/v1/licenses/sessions/${this.sessionId}/heartbeat`,
{
method: 'PATCH',
headers: {
'Authorization': `Bearer ${this.jwtToken}`
}
}
);

if (!response.ok) {
throw new Error(`Heartbeat failed: ${response.statusText}`);
}

return response.json();
}

startHeartbeat() {
// Send heartbeat every 3 minutes
this.heartbeatInterval = setInterval(async () => {
try {
await this.heartbeat();
console.log('Heartbeat sent');
} catch (error) {
console.error('Heartbeat failed:', error);
}
}, 180000); // 3 minutes
}

stopHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
}

async releaseLicense() {
if (!this.sessionId) {
throw new Error('No active session');
}

this.stopHeartbeat();

const response = await fetch(
`${this.baseUrl}/api/v1/licenses/sessions/${this.sessionId}`,
{
method: 'DELETE',
headers: {
'Authorization': `Bearer ${this.jwtToken}`
}
}
);

if (!response.ok) {
throw new Error(`Release failed: ${response.statusText}`);
}

const result = await response.json();
this.sessionId = null;
return result;
}
}

// Usage
const client = new CoditectLicenseClient(
'https://api.coditect.com',
'your-firebase-jwt-token'
);

// Acquire license
const session = await client.acquireLicense(
'PROD-2025-ABCD-1234',
'mac-address-hash-xyz'
);
console.log('Session acquired:', session.id);

// Heartbeat runs automatically in background

// Release when done
window.addEventListener('beforeunload', async () => {
await client.releaseLicense();
});

Additional Resources


Changelog

Version 1.0.0 (2025-11-30)

  • Initial API release
  • License acquisition with atomic seat counting
  • Session heartbeat mechanism
  • License release with audit logging
  • Cloud KMS digital signatures
  • Firebase JWT authentication
  • Comprehensive error handling

Last Updated: November 30, 2025 API Version: 1.0.0 License: Proprietary - AZ1.AI INC