Skip to main content

Container Session API Integration Guide

Task ID: F.5.1 Version: 1.0.0 Last Updated: January 5, 2026 Status: Production Ready


Table of Contents

  1. Overview
  2. Architecture
  3. Authentication
  4. API Endpoints
  5. Common Workflows
  6. Error Handling
  7. Rate Limiting
  8. Multi-Tenant Considerations
  9. TypeScript Integration
  10. Testing

Overview

The Container Session Lifecycle API manages licensed container sessions for CODITECT deployments. It supports:

  • License validation for Docker, Cloud Workstation, and Kubernetes containers
  • Heartbeat-based session management with automatic expiry
  • Multi-user session tracking for shared containers (Cloud Workstations)
  • Admin controls for session termination and user management
  • Real-time statistics for license utilization monitoring

Key Concepts

ConceptDescription
Container SessionA license-validated container instance tracked by heartbeats
User SessionAn individual user within a multi-user container
Session TokenOpaque token used for container-level operations
User TokenOpaque token used for user-level operations
HeartbeatPeriodic signal to keep session active (default: 60s)
TTLTime-to-live before session expires without heartbeat (default: 6 min)

Container Types

TypeMax UsersUse Case
docker1Local developer containers
cloud_workstation1-100GCP Cloud Workstations (shared)
kubernetes1-10Kubernetes pod deployments

Architecture

Session Lifecycle

┌─────────────────────────────────────────────────────────────────┐
│ CONTAINER SESSION LIFECYCLE │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Container Start │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Validate │ ◄── POST /api/v1/sessions/validate/ │
│ │ License │ │
│ └──────┬───────┘ │
│ │ │
│ ┌────┴────┐ │
│ Valid Invalid │
│ │ │ │
│ ▼ ▼ │
│ ┌────┐ ┌───────┐ │
│ │RUN │ │ EXIT │ │
│ └─┬──┘ └───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Heartbeat │ ◄── POST /api/v1/sessions/heartbeat/ │
│ │ Loop │ (every 60s) │
│ └──────┬───────┘ │
│ │ │
│ ┌────┴────┐ │
│ Continue Shutdown │
│ │ │ │
│ │ ▼ │
│ │ ┌──────────┐ │
│ │ │ Release │ ◄── POST /api/v1/sessions/release/ │
│ │ └──────────┘ │
│ │ │
│ └──► (Loop) │
│ │
└─────────────────────────────────────────────────────────────────┘

Multi-User Session Flow

┌─────────────────────────────────────────────────────────────────┐
│ MULTI-USER SESSION FLOW │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Container Session (Cloud Workstation) │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ User Joins │ ◄── POST /api/v1/sessions/user/join/ │
│ │ (User 1) │ │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ User Joins │ ◄── POST /api/v1/sessions/user/join/ │
│ │ (User 2) │ │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ User Leaves │ ◄── POST /api/v1/sessions/user/leave/ │
│ │ (User 1) │ │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ Container Session Remains Active for User 2 │
│ │
└─────────────────────────────────────────────────────────────────┘

Authentication

All API requests require a valid JWT Bearer token.

Request Headers

Authorization: Bearer <JWT_TOKEN>
Content-Type: application/json

Obtaining JWT Token

1. User Login (OAuth2 Password Flow):

curl -X POST https://api.coditect.ai/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "SecurePassword123!"
}'

Response:

{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600
}

2. Use Access Token:

export JWT_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

API Endpoints

Base URL

Production: https://api.coditect.ai/api/v1/sessions
Staging: https://staging-api.coditect.ai/api/v1/sessions

1. Container Validation

Endpoint: POST /api/v1/sessions/validate/

Validates a container's license key and creates or retrieves a container session.

Request

curl -X POST https://api.coditect.ai/api/v1/sessions/validate/ \
-H "Authorization: Bearer $JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"license_key": "CODITECT-XXXX-XXXX-XXXX-XXXX",
"container_id": "container-abc123",
"container_type": "cloud_workstation",
"container_name": "my-dev-environment",
"hostname": "workstation-01.example.com",
"client_version": "1.0.0",
"max_users": 10
}'

Request Body

FieldTypeRequiredDescription
license_keystringYesCustomer license key (format: CODITECT-XXXX-...)
container_idstringYesUnique container identifier (hostname, pod name, etc.)
container_typestringNodocker, cloud_workstation, kubernetes (default: docker)
container_namestringNoHuman-readable container name
hostnamestringNoContainer hostname
client_versionstringNoCODITECT client version
max_usersintegerNoMax concurrent users (default: 1 for docker, 10 for workstation)
workstation_jwtstringNoGCP workstation JWT for OAuth validation

Response (200 OK)

{
"session_id": "cs_1234567890abcdef",
"session_token": "tok_container_abc123xyz...",
"status": "active",
"license_key": "CODITECT-XXXX-XXXX-XXXX-XXXX",
"max_users": 10,
"current_user_count": 0,
"heartbeat_interval_seconds": 60,
"ttl_seconds": 360,
"expires_at": "2026-01-05T12:15:00Z",
"message": "Container session created successfully"
}

Response Fields

FieldTypeDescription
session_idstringUnique session ID (database primary key)
session_tokenstringOpaque token for heartbeat/release operations
statusstringactive, expired, or released
license_keystringValidated license key
max_usersintegerMaximum concurrent users for this session
current_user_countintegerNumber of users currently in session
heartbeat_interval_secondsintegerRecommended heartbeat interval (default: 60)
ttl_secondsintegerSession expiry TTL without heartbeat (default: 360)
expires_atstringISO 8601 timestamp when session will expire
messagestringHuman-readable status message

Error Responses

401 Unauthorized - Invalid License Key:

{
"error": "Invalid license key",
"code": "INVALID_LICENSE",
"details": {
"license_key": "CODITECT-XXXX-XXXX-XXXX-XXXX"
}
}

403 Forbidden - License Expired:

{
"error": "License has expired",
"code": "LICENSE_EXPIRED",
"details": {
"expired_at": "2025-12-31T23:59:59Z"
}
}

409 Conflict - Container Limit Reached:

{
"error": "Container limit reached for this license",
"code": "CONTAINER_LIMIT_REACHED",
"details": {
"limit": 5,
"active_containers": 5
}
}

TypeScript Example

import { containerSessionService } from '@/services/containerSession.service';
import type { ContainerValidateRequest } from '@/types/containerSession';

async function validateContainer() {
try {
const request: ContainerValidateRequest = {
license_key: 'CODITECT-XXXX-XXXX-XXXX-XXXX',
container_id: 'container-abc123',
container_type: 'cloud_workstation',
container_name: 'my-dev-environment',
hostname: 'workstation-01.example.com',
max_users: 10,
};

const response = await containerSessionService.validateContainer(request);

console.log('Session created:', response.session_id);
console.log('Session token:', response.session_token);
console.log('Expires at:', response.expires_at);

// Store session_token for heartbeat operations
localStorage.setItem('container_session_token', response.session_token);

return response;
} catch (error) {
if (error.response?.status === 401) {
console.error('Invalid license key');
} else if (error.response?.status === 403) {
console.error('License expired');
} else if (error.response?.status === 409) {
console.error('Container limit reached');
}
throw error;
}
}

2. Heartbeat

Endpoint: POST /api/v1/sessions/heartbeat/

Sends a heartbeat to keep the container session active. Must be called at least every heartbeat_interval_seconds (default: 60s).

Request

curl -X POST https://api.coditect.ai/api/v1/sessions/heartbeat/ \
-H "Authorization: Bearer $JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"session_token": "tok_container_abc123xyz...",
"active_user_count": 3,
"metadata": {
"cpu_usage": 45.2,
"memory_usage": 2048,
"active_processes": 12
}
}'

Request Body

FieldTypeRequiredDescription
session_tokenstringYesSession token from validation response
active_user_countintegerNoCurrent number of active users (for multi-user containers)
metadataobjectNoOptional metadata (CPU, memory, custom metrics)

Response (200 OK)

{
"status": "active",
"expires_at": "2026-01-05T12:20:00Z",
"current_user_count": 3,
"message": "Heartbeat received successfully"
}

Error Responses

401 Unauthorized - Invalid Session Token:

{
"error": "Invalid or expired session token",
"code": "INVALID_SESSION_TOKEN"
}

404 Not Found - Session Not Found:

{
"error": "Container session not found",
"code": "SESSION_NOT_FOUND"
}

410 Gone - Session Expired:

{
"error": "Container session has expired",
"code": "SESSION_EXPIRED",
"details": {
"expired_at": "2026-01-05T12:00:00Z"
}
}

TypeScript Example

import { containerSessionService } from '@/services/containerSession.service';

// Heartbeat loop
let heartbeatInterval: NodeJS.Timeout;

function startHeartbeat(sessionToken: string, intervalSeconds: number = 60) {
heartbeatInterval = setInterval(async () => {
try {
const response = await containerSessionService.sendHeartbeat({
session_token: sessionToken,
active_user_count: 3,
metadata: {
cpu_usage: getCPUUsage(),
memory_usage: getMemoryUsage(),
},
});

console.log('Heartbeat sent. Expires at:', response.expires_at);
} catch (error) {
console.error('Heartbeat failed:', error);

if (error.response?.status === 410) {
console.error('Session expired. Stopping heartbeat.');
stopHeartbeat();
}
}
}, intervalSeconds * 1000);
}

function stopHeartbeat() {
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
}
}

// Start heartbeat after validation
const validationResponse = await validateContainer();
startHeartbeat(validationResponse.session_token, validationResponse.heartbeat_interval_seconds);

3. Release Session

Endpoint: POST /api/v1/sessions/release/

Gracefully releases a container session. Call this on container shutdown to immediately free the license seat.

Request

curl -X POST https://api.coditect.ai/api/v1/sessions/release/ \
-H "Authorization: Bearer $JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"session_token": "tok_container_abc123xyz...",
"force": false
}'

Request Body

FieldTypeRequiredDescription
session_tokenstringYesSession token from validation response
forcebooleanNoForce release even if users are active (default: false)

Response (200 OK)

{
"released": true,
"session_id": "cs_1234567890abcdef",
"message": "Container session released successfully"
}

TypeScript Example

import { containerSessionService } from '@/services/containerSession.service';

async function releaseSession(sessionToken: string) {
try {
const response = await containerSessionService.releaseContainer({
session_token: sessionToken,
force: false,
});

console.log('Session released:', response.session_id);

// Clean up heartbeat
stopHeartbeat();

// Clear stored token
localStorage.removeItem('container_session_token');
} catch (error) {
console.error('Failed to release session:', error);
throw error;
}
}

// Call on container shutdown
window.addEventListener('beforeunload', () => {
const sessionToken = localStorage.getItem('container_session_token');
if (sessionToken) {
releaseSession(sessionToken);
}
});

4. User Join

Endpoint: POST /api/v1/sessions/user/join/

Adds a user to a multi-user container session (e.g., Cloud Workstation).

Request

curl -X POST https://api.coditect.ai/api/v1/sessions/user/join/ \
-H "Authorization: Bearer $JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"session_token": "tok_container_abc123xyz...",
"username": "john.doe",
"display_name": "John Doe",
"metadata": {
"client_ip": "192.168.1.100",
"client_version": "1.0.0"
}
}'

Request Body

FieldTypeRequiredDescription
session_tokenstringYesContainer session token
usernamestringYesUnique username for this user
display_namestringNoHuman-readable display name
metadataobjectNoOptional user metadata

Response (200 OK)

{
"user_session_id": "us_abcdef1234567890",
"user_token": "tok_user_xyz789...",
"username": "john.doe",
"display_name": "John Doe",
"status": "active",
"joined_at": "2026-01-05T12:10:00Z",
"message": "User joined container session successfully"
}

Error Responses

409 Conflict - User Limit Reached:

{
"error": "Container user limit reached",
"code": "USER_LIMIT_REACHED",
"details": {
"max_users": 10,
"current_users": 10
}
}

409 Conflict - User Already in Session:

{
"error": "User already in this container session",
"code": "USER_ALREADY_IN_SESSION",
"details": {
"username": "john.doe"
}
}

TypeScript Example

import { containerSessionService } from '@/services/containerSession.service';

async function joinSession(sessionToken: string, username: string) {
try {
const response = await containerSessionService.userJoin({
session_token: sessionToken,
username: username,
display_name: 'John Doe',
metadata: {
client_ip: '192.168.1.100',
},
});

console.log('User joined:', response.user_session_id);

// Store user_token for user-level operations
localStorage.setItem('user_session_token', response.user_token);

return response;
} catch (error) {
if (error.response?.status === 409) {
const code = error.response?.data?.code;
if (code === 'USER_LIMIT_REACHED') {
console.error('Container is full');
} else if (code === 'USER_ALREADY_IN_SESSION') {
console.error('User already joined');
}
}
throw error;
}
}

5. User Leave

Endpoint: POST /api/v1/sessions/user/leave/

Removes a user from a multi-user container session.

Request

curl -X POST https://api.coditect.ai/api/v1/sessions/user/leave/ \
-H "Authorization: Bearer $JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"user_token": "tok_user_xyz789..."
}'

Request Body

FieldTypeRequiredDescription
user_tokenstringYesUser token from join response

Response (200 OK)

{
"left": true,
"user_session_id": "us_abcdef1234567890",
"message": "User left container session successfully"
}

TypeScript Example

import { containerSessionService } from '@/services/containerSession.service';

async function leaveSession(userToken: string) {
try {
const response = await containerSessionService.userLeave({
user_token: userToken,
});

console.log('User left:', response.user_session_id);

// Clear stored token
localStorage.removeItem('user_session_token');
} catch (error) {
console.error('Failed to leave session:', error);
throw error;
}
}

// Call on user disconnect
window.addEventListener('beforeunload', () => {
const userToken = localStorage.getItem('user_session_token');
if (userToken) {
leaveSession(userToken);
}
});

6. List Sessions (Admin)

Endpoint: GET /api/v1/sessions/

Lists all container sessions with optional filtering. Access controlled by role.

Request

curl -X GET "https://api.coditect.ai/api/v1/sessions/?status=active&container_type=cloud_workstation&page=1&page_size=20" \
-H "Authorization: Bearer $JWT_TOKEN"

Query Parameters

ParameterTypeDescription
statusstringFilter by status: active, expired, released
container_typestringFilter by type: docker, cloud_workstation, kubernetes
license_idstringFilter by license ID
tenant_idstringFilter by tenant ID (System Admin only)
searchstringSearch by container ID, name, or hostname
pageintegerPage number (default: 1)
page_sizeintegerResults per page (default: 20, max: 100)

Response (200 OK)

{
"count": 42,
"next": "https://api.coditect.ai/api/v1/sessions/?page=2",
"previous": null,
"results": [
{
"id": "cs_1234567890abcdef",
"container_id": "container-abc123",
"container_type": "cloud_workstation",
"container_name": "my-dev-environment",
"license_id": "lic_xyz789",
"license_key": "CODITECT-XXXX-XXXX-XXXX-XXXX",
"session_token": "tok_container_...",
"status": "active",
"max_users": 10,
"current_user_count": 3,
"hostname": "workstation-01.example.com",
"client_version": "1.0.0",
"last_heartbeat": "2026-01-05T12:14:00Z",
"heartbeat_interval_seconds": 60,
"ttl_seconds": 360,
"created_at": "2026-01-05T10:00:00Z",
"updated_at": "2026-01-05T12:14:00Z",
"expires_at": "2026-01-05T12:20:00Z",
"tenant_id": "ten_abc123",
"tenant_name": "Acme Corporation"
}
]
}

TypeScript Example

import { containerSessionService } from '@/services/containerSession.service';
import type { ContainerSessionFilters } from '@/types/containerSession';

async function listSessions() {
try {
const filters: ContainerSessionFilters = {
status: 'active',
container_type: 'cloud_workstation',
page: 1,
page_size: 20,
};

const response = await containerSessionService.listSessions(filters);

console.log(`Total sessions: ${response.count}`);
console.log(`Sessions on this page: ${response.results.length}`);

response.results.forEach(session => {
console.log(`${session.container_name}: ${session.current_user_count}/${session.max_users} users`);
});

return response;
} catch (error) {
console.error('Failed to list sessions:', error);
throw error;
}
}

7. Get Session (Admin)

Endpoint: GET /api/v1/sessions/{id}/

Retrieves a single container session with nested user sessions.

Request

curl -X GET https://api.coditect.ai/api/v1/sessions/cs_1234567890abcdef/ \
-H "Authorization: Bearer $JWT_TOKEN"

Response (200 OK)

{
"id": "cs_1234567890abcdef",
"container_id": "container-abc123",
"container_type": "cloud_workstation",
"container_name": "my-dev-environment",
"license_id": "lic_xyz789",
"license_key": "CODITECT-XXXX-XXXX-XXXX-XXXX",
"session_token": "tok_container_...",
"status": "active",
"max_users": 10,
"current_user_count": 3,
"hostname": "workstation-01.example.com",
"client_version": "1.0.0",
"last_heartbeat": "2026-01-05T12:14:00Z",
"heartbeat_interval_seconds": 60,
"ttl_seconds": 360,
"created_at": "2026-01-05T10:00:00Z",
"updated_at": "2026-01-05T12:14:00Z",
"expires_at": "2026-01-05T12:20:00Z",
"tenant_id": "ten_abc123",
"tenant_name": "Acme Corporation",
"user_sessions": [
{
"id": "us_abc123",
"container_session_id": "cs_1234567890abcdef",
"user_id": "user_xyz789",
"user_token": "tok_user_...",
"username": "john.doe",
"display_name": "John Doe",
"status": "active",
"metadata": {},
"last_activity": "2026-01-05T12:14:00Z",
"joined_at": "2026-01-05T10:05:00Z",
"left_at": null
}
]
}

TypeScript Example

import { containerSessionService } from '@/services/containerSession.service';

async function getSession(sessionId: string) {
try {
const session = await containerSessionService.getSession(sessionId);

console.log(`Session: ${session.container_name}`);
console.log(`Users: ${session.current_user_count}/${session.max_users}`);

session.user_sessions?.forEach(userSession => {
console.log(` - ${userSession.display_name} (${userSession.status})`);
});

return session;
} catch (error) {
if (error.response?.status === 404) {
console.error('Session not found');
}
throw error;
}
}

8. Terminate Session (Admin)

Endpoint: DELETE /api/v1/sessions/{id}/

Force-terminates a container session. Disconnects all users immediately.

Request

curl -X DELETE https://api.coditect.ai/api/v1/sessions/cs_1234567890abcdef/ \
-H "Authorization: Bearer $JWT_TOKEN"

Response (204 No Content)

No response body. HTTP 204 indicates successful termination.

Error Responses

403 Forbidden - Insufficient Permissions:

{
"error": "You do not have permission to terminate this session",
"code": "PERMISSION_DENIED"
}

404 Not Found:

{
"error": "Container session not found",
"code": "SESSION_NOT_FOUND"
}

TypeScript Example

import { containerSessionService } from '@/services/containerSession.service';

async function terminateSession(sessionId: string) {
try {
await containerSessionService.terminateSession(sessionId);

console.log('Session terminated successfully');
} catch (error) {
if (error.response?.status === 403) {
console.error('Insufficient permissions to terminate session');
} else if (error.response?.status === 404) {
console.error('Session not found');
}
throw error;
}
}

9. Kick User (Admin)

Endpoint: DELETE /api/v1/sessions/{id}/users/{user_id}/

Force-disconnects a specific user from a container session. The container session remains active for other users.

Request

curl -X DELETE https://api.coditect.ai/api/v1/sessions/cs_1234567890abcdef/users/us_abc123/ \
-H "Authorization: Bearer $JWT_TOKEN"

Response (204 No Content)

No response body. HTTP 204 indicates successful kick.

TypeScript Example

import { containerSessionService } from '@/services/containerSession.service';

async function kickUser(containerSessionId: string, userSessionId: string) {
try {
await containerSessionService.kickUser(containerSessionId, userSessionId);

console.log('User kicked successfully');
} catch (error) {
if (error.response?.status === 403) {
console.error('Insufficient permissions to kick user');
} else if (error.response?.status === 404) {
console.error('Session or user not found');
}
throw error;
}
}

10. Statistics

Endpoint: GET /api/v1/sessions/stats/

Returns aggregate statistics for the current tenant's container sessions.

Request

curl -X GET https://api.coditect.ai/api/v1/sessions/stats/ \
-H "Authorization: Bearer $JWT_TOKEN"

Response (200 OK)

{
"total_sessions": 42,
"active_sessions": 35,
"expired_sessions": 5,
"released_sessions": 2,
"total_users": 127,
"active_users": 104,
"by_container_type": {
"docker": 10,
"cloud_workstation": 20,
"kubernetes": 5
},
"by_status": {
"active": 35,
"expired": 5,
"released": 2
},
"avg_users_per_session": 3.6,
"avg_session_duration_minutes": 180.5
}

TypeScript Example

import { containerSessionService } from '@/services/containerSession.service';

async function getStats() {
try {
const stats = await containerSessionService.getStats();

console.log(`Active Sessions: ${stats.active_sessions}`);
console.log(`Active Users: ${stats.active_users}`);
console.log(`Avg Users/Session: ${stats.avg_users_per_session.toFixed(1)}`);

return stats;
} catch (error) {
console.error('Failed to fetch stats:', error);
throw error;
}
}

Common Workflows

Workflow 1: Container Startup and Validation

Code Example:

import { containerSessionService } from '@/services/containerSession.service';

async function startupContainer() {
// Step 1: Validate container
const validationResponse = await containerSessionService.validateContainer({
license_key: process.env.CODITECT_LICENSE_KEY!,
container_id: process.env.HOSTNAME!,
container_type: 'cloud_workstation',
max_users: 10,
});

// Step 2: Store session token
const sessionToken = validationResponse.session_token;
localStorage.setItem('container_session_token', sessionToken);

// Step 3: Start heartbeat loop
const heartbeatInterval = validationResponse.heartbeat_interval_seconds;
startHeartbeat(sessionToken, heartbeatInterval);

console.log('Container started successfully');
}

Workflow 2: User Join Multi-User Container

Code Example:

import { containerSessionService } from '@/services/containerSession.service';

async function userConnect(username: string) {
const containerSessionToken = localStorage.getItem('container_session_token');

if (!containerSessionToken) {
throw new Error('Container session not initialized');
}

// Step 1: User joins session
const userJoinResponse = await containerSessionService.userJoin({
session_token: containerSessionToken,
username: username,
display_name: getUserDisplayName(username),
});

// Step 2: Store user token
const userToken = userJoinResponse.user_token;
localStorage.setItem('user_session_token', userToken);

console.log(`User ${username} joined successfully`);

// Step 3: Setup cleanup on disconnect
window.addEventListener('beforeunload', async () => {
await containerSessionService.userLeave({ user_token: userToken });
});
}

Workflow 3: Heartbeat Loop with Reconnection

import { containerSessionService } from '@/services/containerSession.service';

let heartbeatInterval: NodeJS.Timeout | null = null;
let heartbeatFailureCount = 0;
const MAX_FAILURES = 3;

async function sendHeartbeat(sessionToken: string) {
try {
const response = await containerSessionService.sendHeartbeat({
session_token: sessionToken,
active_user_count: getCurrentUserCount(),
});

// Reset failure count on success
heartbeatFailureCount = 0;

console.log(`Heartbeat sent. Expires at: ${response.expires_at}`);
} catch (error) {
heartbeatFailureCount++;

console.error(`Heartbeat failed (${heartbeatFailureCount}/${MAX_FAILURES}):`, error);

if (error.response?.status === 410) {
// Session expired
console.error('Session expired. Stopping heartbeat.');
stopHeartbeat();
await handleSessionExpiry();
} else if (heartbeatFailureCount >= MAX_FAILURES) {
// Too many failures
console.error('Max heartbeat failures reached. Stopping.');
stopHeartbeat();
await handleHeartbeatFailure();
}
}
}

function startHeartbeat(sessionToken: string, intervalSeconds: number = 60) {
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
}

heartbeatInterval = setInterval(() => {
sendHeartbeat(sessionToken);
}, intervalSeconds * 1000);

// Send initial heartbeat immediately
sendHeartbeat(sessionToken);
}

function stopHeartbeat() {
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
heartbeatInterval = null;
}
}

async function handleSessionExpiry() {
// Attempt to re-validate
try {
const validationResponse = await containerSessionService.validateContainer({
license_key: process.env.CODITECT_LICENSE_KEY!,
container_id: process.env.HOSTNAME!,
container_type: 'cloud_workstation',
});

// Restart heartbeat with new session
startHeartbeat(validationResponse.session_token, validationResponse.heartbeat_interval_seconds);

console.log('Session re-validated successfully');
} catch (error) {
console.error('Failed to re-validate session:', error);
// Shutdown container
process.exit(1);
}
}

async function handleHeartbeatFailure() {
// Log error and attempt recovery
console.error('Heartbeat loop failed. Attempting recovery...');
// Implement retry logic or alert admin
}

Workflow 4: Graceful Container Shutdown

import { containerSessionService } from '@/services/containerSession.service';

async function shutdownContainer() {
const sessionToken = localStorage.getItem('container_session_token');

if (!sessionToken) {
console.warn('No active session to release');
return;
}

try {
// Step 1: Stop heartbeat
stopHeartbeat();

// Step 2: Disconnect all users
const userToken = localStorage.getItem('user_session_token');
if (userToken) {
await containerSessionService.userLeave({ user_token: userToken });
}

// Step 3: Release container session
await containerSessionService.releaseContainer({
session_token: sessionToken,
force: false,
});

// Step 4: Cleanup
localStorage.removeItem('container_session_token');
localStorage.removeItem('user_session_token');

console.log('Container shutdown complete');
} catch (error) {
console.error('Error during shutdown:', error);
// Continue shutdown even if release fails
}
}

// Register shutdown handler
process.on('SIGTERM', shutdownContainer);
process.on('SIGINT', shutdownContainer);
window.addEventListener('beforeunload', shutdownContainer);

Error Handling

Error Response Format

All API errors follow this structure:

{
"error": "Human-readable error message",
"code": "ERROR_CODE",
"details": {
"key": "Additional context"
}
}

Error Codes

HTTPCodeDescriptionRecommended Action
400VALIDATION_ERRORInvalid request dataFix request payload
401INVALID_LICENSEInvalid license keyCheck license key format
401INVALID_SESSION_TOKENInvalid or expired session tokenRe-validate container
401INVALID_USER_TOKENInvalid or expired user tokenRe-join session
403LICENSE_EXPIREDLicense has expiredRenew license
403PERMISSION_DENIEDInsufficient permissionsCheck user role
404SESSION_NOT_FOUNDSession doesn't existVerify session ID
409CONTAINER_LIMIT_REACHEDToo many containersRelease inactive sessions
409USER_LIMIT_REACHEDToo many users in containerWait for users to leave
409USER_ALREADY_IN_SESSIONUser already joinedUse existing user session
410SESSION_EXPIREDSession expired (TTL)Re-validate container
429RATE_LIMIT_EXCEEDEDToo many requestsImplement backoff
500INTERNAL_SERVER_ERRORServer errorRetry with exponential backoff

Error Handling Best Practices

import { AxiosError } from 'axios';
import type { ContainerSessionError } from '@/types/containerSession';

async function handleAPICall<T>(apiCall: () => Promise<T>): Promise<T> {
try {
return await apiCall();
} catch (error) {
if (error instanceof AxiosError) {
const apiError = error.response?.data as ContainerSessionError;

switch (apiError.code) {
case 'INVALID_LICENSE':
case 'LICENSE_EXPIRED':
// Critical: Cannot continue
console.error('License validation failed:', apiError.error);
await shutdownContainer();
throw error;

case 'SESSION_EXPIRED':
case 'INVALID_SESSION_TOKEN':
// Recoverable: Re-validate
console.warn('Session expired. Re-validating...');
await revalidateSession();
return await apiCall(); // Retry

case 'CONTAINER_LIMIT_REACHED':
case 'USER_LIMIT_REACHED':
// User action required
console.error('Capacity limit reached:', apiError.error);
throw error;

case 'RATE_LIMIT_EXCEEDED':
// Implement backoff
const retryAfter = parseInt(error.response?.headers['retry-after'] || '60');
console.warn(`Rate limited. Retrying after ${retryAfter}s`);
await sleep(retryAfter * 1000);
return await apiCall(); // Retry

default:
// Unknown error
console.error('API error:', apiError);
throw error;
}
}

// Non-Axios error
throw error;
}
}

function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}

Rate Limiting

Rate Limits

EndpointLimitWindowScope
/validate/10 requests1 minutePer license key
/heartbeat/120 requests1 minutePer session token
/release/10 requests1 minutePer session token
/user/join/30 requests1 minutePer session token
/user/leave/30 requests1 minutePer user token
Admin endpoints60 requests1 minutePer user

Rate Limit Headers

X-RateLimit-Limit: 120
X-RateLimit-Remaining: 115
X-RateLimit-Reset: 1736087400
Retry-After: 45

Handling Rate Limits

import { containerSessionService } from '@/services/containerSession.service';

async function sendHeartbeatWithBackoff(sessionToken: string) {
const maxRetries = 3;
let retryCount = 0;

while (retryCount < maxRetries) {
try {
const response = await containerSessionService.sendHeartbeat({
session_token: sessionToken,
});

return response;
} catch (error) {
if (error.response?.status === 429) {
const retryAfter = parseInt(error.response?.headers['retry-after'] || '60');

console.warn(`Rate limited. Retrying after ${retryAfter}s (attempt ${retryCount + 1}/${maxRetries})`);

await sleep(retryAfter * 1000);
retryCount++;
} else {
throw error;
}
}
}

throw new Error('Max retries exceeded for rate limit');
}

Multi-Tenant Considerations

Tenant Isolation

All API responses are automatically filtered by the authenticated user's tenant. Requests cannot access data from other tenants.

RoleAccess Scope
System AdminAll tenants (requires tenant_id filter)
Tenant AdminOwn tenant only
Team ManagerOwn team's sessions only
UserOwn sessions only

Tenant Context Headers

X-Tenant-ID: ten_abc123
X-Tenant-Name: Acme Corporation

System Admin Cross-Tenant Access

System admins can query across tenants by specifying tenant_id:

curl -X GET "https://api.coditect.ai/api/v1/sessions/?tenant_id=ten_xyz789" \
-H "Authorization: Bearer $SYSTEM_ADMIN_JWT_TOKEN"

TypeScript Example:

import { containerSessionService } from '@/services/containerSession.service';
import { useAuth } from '@/hooks/useAuth';

async function listSessionsForTenant(tenantId?: string) {
const { isSystemAdmin } = useAuth();

if (!isSystemAdmin && tenantId) {
throw new Error('Only System Admins can filter by tenant_id');
}

const filters = {
tenant_id: isSystemAdmin ? tenantId : undefined,
status: 'active',
};

return await containerSessionService.listSessions(filters);
}

TypeScript Integration

Complete Integration Example

// src/lib/container-session.ts
import { containerSessionService } from '@/services/containerSession.service';
import type {
ContainerValidateRequest,
ContainerValidateResponse,
ContainerHeartbeatRequest,
} from '@/types/containerSession';

class ContainerSessionManager {
private sessionToken: string | null = null;
private heartbeatInterval: NodeJS.Timeout | null = null;
private heartbeatIntervalSeconds: number = 60;

/**
* Initialize container session
*/
async initialize(licenseKey: string, containerId: string): Promise<void> {
const request: ContainerValidateRequest = {
license_key: licenseKey,
container_id: containerId,
container_type: 'cloud_workstation',
max_users: 10,
};

const response: ContainerValidateResponse = await containerSessionService.validateContainer(request);

this.sessionToken = response.session_token;
this.heartbeatIntervalSeconds = response.heartbeat_interval_seconds;

// Store in localStorage
localStorage.setItem('container_session_token', this.sessionToken);

// Start heartbeat
this.startHeartbeat();

console.log('Container session initialized:', response.session_id);
}

/**
* Start heartbeat loop
*/
private startHeartbeat(): void {
if (!this.sessionToken) {
throw new Error('Cannot start heartbeat without session token');
}

if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
}

this.heartbeatInterval = setInterval(() => {
this.sendHeartbeat();
}, this.heartbeatIntervalSeconds * 1000);

// Send initial heartbeat immediately
this.sendHeartbeat();
}

/**
* Send heartbeat
*/
private async sendHeartbeat(): Promise<void> {
if (!this.sessionToken) return;

try {
const request: ContainerHeartbeatRequest = {
session_token: this.sessionToken,
active_user_count: this.getCurrentUserCount(),
};

const response = await containerSessionService.sendHeartbeat(request);

console.log(`Heartbeat sent. Expires at: ${response.expires_at}`);
} catch (error) {
console.error('Heartbeat failed:', error);

if (error.response?.status === 410) {
// Session expired - attempt re-validation
await this.handleSessionExpiry();
}
}
}

/**
* Handle session expiry
*/
private async handleSessionExpiry(): Promise<void> {
console.warn('Session expired. Attempting re-validation...');

try {
const licenseKey = process.env.CODITECT_LICENSE_KEY!;
const containerId = process.env.HOSTNAME!;

await this.initialize(licenseKey, containerId);
} catch (error) {
console.error('Re-validation failed:', error);
process.exit(1);
}
}

/**
* Get current user count
*/
private getCurrentUserCount(): number {
// Implement your logic to track active users
return 1;
}

/**
* Release session
*/
async release(): Promise<void> {
if (!this.sessionToken) return;

// Stop heartbeat
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}

try {
await containerSessionService.releaseContainer({
session_token: this.sessionToken,
});

console.log('Container session released');
} catch (error) {
console.error('Failed to release session:', error);
}

// Cleanup
this.sessionToken = null;
localStorage.removeItem('container_session_token');
}
}

// Singleton instance
export const containerSession = new ContainerSessionManager();

// Initialize on container start
if (process.env.NODE_ENV === 'production') {
const licenseKey = process.env.CODITECT_LICENSE_KEY;
const containerId = process.env.HOSTNAME;

if (licenseKey && containerId) {
containerSession.initialize(licenseKey, containerId);
}
}

// Release on shutdown
process.on('SIGTERM', () => containerSession.release());
process.on('SIGINT', () => containerSession.release());

Testing

Unit Tests

// src/services/__tests__/containerSession.service.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { containerSessionService } from '@/services/containerSession.service';
import api from '@/lib/api';

vi.mock('@/lib/api');

describe('containerSessionService', () => {
beforeEach(() => {
vi.clearAllMocks();
});

describe('validateContainer', () => {
it('should validate container successfully', async () => {
const mockResponse = {
session_id: 'cs_123',
session_token: 'tok_abc',
status: 'active',
license_key: 'CODITECT-XXXX',
max_users: 10,
current_user_count: 0,
heartbeat_interval_seconds: 60,
ttl_seconds: 360,
expires_at: '2026-01-05T12:00:00Z',
message: 'Success',
};

vi.mocked(api.post).mockResolvedValueOnce({ data: mockResponse });

const result = await containerSessionService.validateContainer({
license_key: 'CODITECT-XXXX',
container_id: 'container-123',
});

expect(result).toEqual(mockResponse);
expect(api.post).toHaveBeenCalledWith('/sessions/validate/', {
license_key: 'CODITECT-XXXX',
container_id: 'container-123',
});
});

it('should handle invalid license error', async () => {
vi.mocked(api.post).mockRejectedValueOnce({
response: {
status: 401,
data: {
error: 'Invalid license key',
code: 'INVALID_LICENSE',
},
},
});

await expect(
containerSessionService.validateContainer({
license_key: 'INVALID',
container_id: 'container-123',
})
).rejects.toThrow();
});
});

describe('sendHeartbeat', () => {
it('should send heartbeat successfully', async () => {
const mockResponse = {
status: 'active',
expires_at: '2026-01-05T12:01:00Z',
current_user_count: 3,
message: 'Heartbeat received',
};

vi.mocked(api.post).mockResolvedValueOnce({ data: mockResponse });

const result = await containerSessionService.sendHeartbeat({
session_token: 'tok_abc',
active_user_count: 3,
});

expect(result).toEqual(mockResponse);
});
});
});

Integration Tests

// src/services/__tests__/containerSession.integration.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { containerSessionService } from '@/services/containerSession.service';

describe('Container Session Integration', () => {
let sessionToken: string;

beforeAll(async () => {
// Validate container
const response = await containerSessionService.validateContainer({
license_key: process.env.TEST_LICENSE_KEY!,
container_id: 'test-container-' + Date.now(),
container_type: 'docker',
});

sessionToken = response.session_token;
});

afterAll(async () => {
// Release session
if (sessionToken) {
await containerSessionService.releaseContainer({
session_token: sessionToken,
});
}
});

it('should complete full session lifecycle', async () => {
// Send heartbeat
const heartbeatResponse = await containerSessionService.sendHeartbeat({
session_token: sessionToken,
});

expect(heartbeatResponse.status).toBe('active');

// Release session
const releaseResponse = await containerSessionService.releaseContainer({
session_token: sessionToken,
});

expect(releaseResponse.released).toBe(true);
});
});

Appendix: API Endpoint Summary

EndpointMethodPurposeAuth Required
/validate/POSTValidate license and create sessionYes
/heartbeat/POSTSend heartbeat to keep session aliveYes
/release/POSTRelease container sessionYes
/user/join/POSTUser joins multi-user containerYes
/user/leave/POSTUser leaves multi-user containerYes
/GETList container sessions (admin)Yes
/{id}/GETGet single container session (admin)Yes
/{id}/DELETETerminate container session (admin)Yes (admin)
/{id}/users/{user_id}/DELETEKick user from session (admin)Yes (admin)
/stats/GETGet session statisticsYes


Compliance: CODITECT Documentation Standard v1.0.0 Owner: AZ1.AI INC Lead: Hal Casteel Version: 1.0.0 Last Updated: January 5, 2026