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
- Acquire a license seat (POST /api/v1/licenses/acquire)
- Maintain the session with heartbeats every 3-5 minutes (PATCH /api/v1/licenses/sessions/{id}/heartbeat)
- 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:
| Field | Type | Required | Description |
|---|---|---|---|
license_key | string | Yes | License key in format XXXX-XXXX-XXXX-XXXX |
hardware_id | string | Yes | Unique hardware identifier (e.g., hashed MAC address) |
ip_address | string (IP) | No | Client IP address (auto-detected if not provided) |
user_agent | string | No | Client 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 Code | Description | Example Response |
|---|---|---|
| 400 Bad Request | Invalid request data | {"license_key": ["Invalid license key."]} |
| 401 Unauthorized | Missing or invalid authentication | {"detail": "Authentication credentials were not provided."} |
| 409 Conflict | No available seats | {"error": "No available seats", "detail": "Maximum concurrent seats (10) reached for your organization"} |
| 503 Service Unavailable | Redis offline | {"error": "License service unavailable (Redis offline)"} |
| 500 Internal Server Error | Server 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:
| Parameter | Type | Required | Description |
|---|---|---|---|
session_id | UUID | Yes | UUID 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 Code | Description | Example Response |
|---|---|---|
| 400 Bad Request | Session already ended | {"error": "Session already ended"} |
| 401 Unauthorized | Missing or invalid authentication | {"detail": "Authentication credentials were not provided."} |
| 404 Not Found | Session not found | {"error": "Session not found"} |
| 410 Gone | Session expired in Redis | {"error": "Session expired or not found in active pool"} |
| 503 Service Unavailable | Redis offline | {"error": "License service unavailable (Redis offline)"} |
| 500 Internal Server Error | Server 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:
| Parameter | Type | Required | Description |
|---|---|---|---|
session_id | UUID | Yes | UUID 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 Code | Description | Example Response |
|---|---|---|
| 401 Unauthorized | Missing or invalid authentication | {"detail": "Authentication credentials were not provided."} |
| 404 Not Found | Session not found | {"error": "Session not found"} |
| 503 Service Unavailable | Redis offline | {"error": "License service unavailable (Redis offline)"} |
| 500 Internal Server Error | Server 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 Category | Limit |
|---|---|
| Authentication | 5 requests/minute |
| License Operations | 100 requests/minute |
| Heartbeat | 20 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
- Swagger UI: http://localhost:8000/api/docs/ (Development)
- ReDoc: http://localhost:8000/api/redoc/ (Development)
- OpenAPI Schema: http://localhost:8000/api/schema/
- Support: api-support@coditect.com
- Documentation: https://docs.coditect.com/api
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