Integration Guide

Guide for application developers integrating with the Riptide Application Manager API.

Table of Contents

Overview

The Riptide Application Manager provides:

  • Identity Management: Trial users, applications, sessions, roles
  • Configuration Management: Application settings, file versioning, rollbacks

Base URLs

  • API: http://localhost:11402 (development) or https://api.your-domain.com (production)
  • Web UI: http://localhost:11401 (development) or https://app.your-domain.com (production)

Note: Application Manager listens on HTTP. In production, place it behind a reverse proxy (NGINX, Caddy, Azure App Gateway, etc.) that terminates TLS and forwards traffic over the internal network. This keeps certificate management in one place and ensures all client traffic is encrypted in transit.

API Documentation

Full API reference available at api-reference.md

Authentication

API Key

All API requests require an API key passed in the X-Api-Key header:

curl -X GET http://localhost:11402/api/users \
  -H "X-Api-Key: rtk_your-api-key"

API keys are issued and managed through the Application Manager Web UI under each application's API Keys page. Each key has a unique prefix (e.g. rtk_abc123) for identification. The raw key value is displayed once at creation and cannot be retrieved again — store it securely.

JWT Authentication

The SDK's Identity component issues RS256-signed JWT access tokens and refresh tokens. API requests can authenticate with a Bearer token in the Authorization header:

curl -X GET http://localhost:11402/api/users \
  -H "Authorization: Bearer eyJhbG..."

Access tokens contain the user's identity, email, and any custom claims. Token lifetimes are configurable via AccessTokenExpirationMinutes and RefreshTokenExpirationDays in the identity configuration. Refresh tokens are cryptographically random and can be exchanged for new access tokens without re-authentication.

In production environments, the service requires explicit RSA key configuration — auto-generated keys are not permitted.

SDK Libraries

using Riptide.ApplicationManager.Client;

// Initialize client
var client = new ApplicationManagerClient(new ApplicationManagerOptions
{
    BaseUrl = "http://localhost:11402",
    ApiKey = "rtk_your-api-key"
});

// Get trial user
var user = await client.TrialUsers.GetByEmailAsync("user@example.com");

// Get configuration
var config = await client.Configuration.GetFileAsync("my-app", "appsettings.json");

HTTP Clients (Any Language)

For languages without an SDK, use standard HTTP libraries:

Python:

import requests

headers = {
    "X-Api-Key": "rtk_your-api-key",
    "Content-Type": "application/json"
}

response = requests.get(
    "http://localhost:11402/api/users",
    headers=headers
)

JavaScript/Node.js:

const axios = require('axios');

const client = axios.create({
    baseURL: 'http://localhost:11402',
    headers: {
        'X-Api-Key': 'rtk_your-api-key'
    }
});

const users = await client.get('/api/users');

Common Integration Patterns

1. Trial User Validation

Check if a user has an active trial before granting access:

public async Task<bool> ValidateTrialAccessAsync(string email)
{
    var user = await _appManagerClient.TrialUsers.GetByEmailAsync(email);
    
    if (user == null || !user.IsActive)
        return false;
        
    if (user.Trial.Status != TrialStatus.Active)
        return false;
        
    if (user.Trial.EndDate < DateTime.UtcNow)
        return false;
        
    return true;
}

2. Configuration Loading

Load application configuration on startup:

public async Task<AppConfiguration> LoadConfigurationAsync(string appName)
{
    var configFile = await _appManagerClient.Configuration
        .GetFileAsync(appName, "appsettings.json");
        
    return JsonSerializer.Deserialize<AppConfiguration>(configFile.Content);
}

3. Session Tracking

Track user sessions for analytics and security:

public async Task<Session> StartUserSessionAsync(int userId, string ipAddress)
{
    var session = await _appManagerClient.Sessions.CreateAsync(new CreateSessionRequest
    {
        TrialUserId = userId,
        IpAddress = ipAddress,
        UserAgent = GetUserAgent(),
        SessionStartedAt = DateTime.UtcNow
    });
    
    return session;
}

public async Task EndUserSessionAsync(int sessionId)
{
    await _appManagerClient.Sessions.EndAsync(sessionId);
}

Trial User Management

Check Trial Status

GET /api/trial-users/{id}
X-Api-Key: rtk_your-api-key

Response:

{
  "id": 1,
  "email": "user@example.com",
  "firstName": "John",
  "lastName": "Doe",
  "isActive": true,
  "trial": {
    "id": 1,
    "status": "Active",
    "startDate": "2026-01-20T00:00:00Z",
    "endDate": "2026-01-27T00:00:00Z",
    "daysRemaining": 0,
    "isExpired": false
  }
}

Create Trial User

POST /api/trial-users
Content-Type: application/json
X-Api-Key: rtk_your-api-key

{
  "email": "newuser@example.com",
  "firstName": "Jane",
  "lastName": "Smith",
  "companyName": "Acme Corp",
  "applicationId": 1
}

Extend Trial

PATCH /api/trials/{id}/extend
Content-Type: application/json
X-Api-Key: rtk_your-api-key

{
  "additionalDays": 7
}

Configuration Management

Get Configuration File

GET /api/configuration/{appName}/files/{fileName}
X-Api-Key: rtk_your-api-key

Response:

{
  "id": 1,
  "fileName": "appsettings.json",
  "content": "{ \"key\": \"value\" }",
  "version": 3,
  "createdAt": "2026-01-27T10:00:00Z"
}

Update Configuration

PUT /api/configuration/{appName}/files/{fileName}
Content-Type: application/json
X-Api-Key: rtk_your-api-key

{
  "content": "{ \"key\": \"new-value\" }"
}

Rollback Configuration

POST /api/configuration/{appName}/files/{fileName}/rollback/{version}
X-Api-Key: rtk_your-api-key

Session Management

Create Session

POST /api/sessions
Content-Type: application/json
X-Api-Key: rtk_your-api-key

{
  "trialUserId": 1,
  "ipAddress": "192.168.1.100",
  "userAgent": "Mozilla/5.0...",
  "sessionStartedAt": "2026-01-27T10:00:00Z"
}

End Session

DELETE /api/sessions/{id}
X-Api-Key: rtk_your-api-key

Error Handling

Standard Error Response

The API uses RFC 7807 Problem Details for error responses:

{
  "type": "https://tools.ietf.org/html/rfc7807#section-3.1",
  "title": "Validation Error",
  "status": 400,
  "detail": "Email is required",
  "traceId": "00-abc123..."
}

Common Error Codes

  • VALIDATION_ERROR (400): Invalid request data
  • UNAUTHORIZED (401): Missing or invalid API key
  • NOT_FOUND (404): Resource not found
  • CONFLICT (409): Resource already exists
  • INTERNAL_ERROR (500): Server error

Error Handling Example

try
{
    var user = await client.TrialUsers.GetByEmailAsync(email);
}
catch (ApplicationManagerException ex) when (ex.StatusCode == 404)
{
    // User not found - create new trial
    var newUser = await client.TrialUsers.CreateAsync(request);
}
catch (ApplicationManagerException ex) when (ex.StatusCode == 401)
{
    // Authentication failed - check API key
    _logger.LogError("Invalid API key");
    throw;
}
catch (ApplicationManagerException ex)
{
    // Other API error
    _logger.LogError(ex, "API error: {Code}", ex.ErrorCode);
    throw;
}

Best Practices

1. Cache Configuration

Don't fetch configuration on every request:

private IMemoryCache _cache;
private TimeSpan _cacheDuration = TimeSpan.FromMinutes(5);

public async Task<AppConfiguration> GetConfigurationAsync(string appName)
{
    return await _cache.GetOrCreateAsync($"config:{appName}", async entry =>
    {
        entry.AbsoluteExpirationRelativeToNow = _cacheDuration;
        return await LoadConfigurationFromApiAsync(appName);
    });
}

2. Validate Trial Status Periodically

Check trial status at application startup and periodically during runtime:

// On startup
var isValid = await ValidateTrialAccessAsync(userEmail);
if (!isValid)
{
    throw new TrialExpiredException("Your trial has expired");
}

// Background validation every hour
var timer = new Timer(async _ => 
{
    await ValidateTrialAccessAsync(userEmail);
}, null, TimeSpan.FromHours(1), TimeSpan.FromHours(1));

3. Handle Network Failures Gracefully

public async Task<T> CallApiWithRetryAsync<T>(Func<Task<T>> apiCall)
{
    var retryPolicy = Policy
        .Handle<HttpRequestException>()
        .WaitAndRetryAsync(3, retryAttempt => 
            TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
    
    return await retryPolicy.ExecuteAsync(apiCall);
}

4. Log API Interactions

_logger.LogInformation("Validating trial access for {Email}", email);
var user = await client.TrialUsers.GetByEmailAsync(email);
_logger.LogInformation("Trial status: {Status}, Days remaining: {Days}", 
    user.Trial.Status, user.Trial.DaysRemaining);

5. Use Environment Variables for Configuration

Never hardcode secrets:

var options = new ApplicationManagerOptions
{
    BaseUrl = Environment.GetEnvironmentVariable("APP_MANAGER_URL") 
        ?? "http://localhost:11402",
    ApiKey = Environment.GetEnvironmentVariable("APP_MANAGER_API_KEY")
        ?? throw new InvalidOperationException("APP_MANAGER_API_KEY not set")
};

Code Examples

Complete Integration Example (.NET)

using Microsoft.Extensions.DependencyInjection;
using Riptide.ApplicationManager.Client;

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Register Application Manager client
        services.AddSingleton<IApplicationManagerClient>(sp =>
        {
            var options = new ApplicationManagerOptions
            {
                BaseUrl = Configuration["ApplicationManager:BaseUrl"],
                ApiKey = Configuration["ApplicationManager:ApiKey"]
            };
            return new ApplicationManagerClient(options);
        });
        
        // Register your services
        services.AddScoped<ITrialValidator, TrialValidator>();
    }
}

public interface ITrialValidator
{
    Task<TrialValidationResult> ValidateAsync(string email);
}

public class TrialValidator : ITrialValidator
{
    private readonly IApplicationManagerClient _client;
    private readonly ILogger<TrialValidator> _logger;
    
    public TrialValidator(
        IApplicationManagerClient client,
        ILogger<TrialValidator> logger)
    {
        _client = client;
        _logger = logger;
    }
    
    public async Task<TrialValidationResult> ValidateAsync(string email)
    {
        try
        {
            var user = await _client.TrialUsers.GetByEmailAsync(email);
            
            if (user == null)
            {
                return TrialValidationResult.NotFound();
            }
            
            if (!user.IsActive)
            {
                return TrialValidationResult.Inactive();
            }
            
            if (user.Trial.IsExpired)
            {
                return TrialValidationResult.Expired(user.Trial.EndDate);
            }
            
            return TrialValidationResult.Valid(user, user.Trial.DaysRemaining);
        }
        catch (ApplicationManagerException ex)
        {
            _logger.LogError(ex, "Error validating trial for {Email}", email);
            return TrialValidationResult.Error(ex.Message);
        }
    }
}

public class TrialValidationResult
{
    public bool IsValid { get; init; }
    public string Message { get; init; }
    public int? DaysRemaining { get; init; }
    
    public static TrialValidationResult Valid(TrialUser user, int daysRemaining) =>
        new() { IsValid = true, Message = "Trial is active", DaysRemaining = daysRemaining };
    
    public static TrialValidationResult NotFound() =>
        new() { IsValid = false, Message = "User not found" };
    
    public static TrialValidationResult Inactive() =>
        new() { IsValid = false, Message = "User account is inactive" };
    
    public static TrialValidationResult Expired(DateTime endDate) =>
        new() { IsValid = false, Message = $"Trial expired on {endDate:yyyy-MM-dd}" };
    
    public static TrialValidationResult Error(string message) =>
        new() { IsValid = false, Message = $"Validation error: {message}" };
}

Python Integration Example

import requests
from datetime import datetime
from typing import Optional, Dict, Any

class ApplicationManagerClient:
    def __init__(self, base_url: str, api_key: str):
        self.base_url = base_url
        self.headers = {
            'X-Api-Key': api_key,
            'Content-Type': 'application/json'
        }
    
    def get_trial_user(self, email: str) -> Optional[Dict[str, Any]]:
        """Get trial user by email"""
        response = requests.get(
            f'{self.base_url}/api/trial-users/by-email/{email}',
            headers=self.headers
        )
        
        if response.status_code == 404:
            return None
        
        response.raise_for_status()
        return response.json()
    
    def validate_trial(self, email: str) -> Dict[str, Any]:
        """Validate trial access"""
        user = self.get_trial_user(email)
        
        if not user:
            return {
                'valid': False,
                'message': 'User not found'
            }
        
        if not user['isActive']:
            return {
                'valid': False,
                'message': 'User account is inactive'
            }
        
        trial = user['trial']
        if trial['isExpired']:
            return {
                'valid': False,
                'message': f"Trial expired on {trial['endDate']}"
            }
        
        return {
            'valid': True,
            'message': 'Trial is active',
            'days_remaining': trial['daysRemaining']
        }
    
    def get_configuration(self, app_name: str, file_name: str) -> str:
        """Get configuration file content"""
        response = requests.get(
            f'{self.base_url}/api/configuration/{app_name}/files/{file_name}',
            headers=self.headers
        )
        response.raise_for_status()
        return response.json()['content']

# Usage
client = ApplicationManagerClient(
    base_url='http://localhost:11402',
    api_key='rtk_your-api-key'
)

# Validate trial
result = client.validate_trial('user@example.com')
if result['valid']:
    print(f"Access granted. {result['days_remaining']} days remaining.")
else:
    print(f"Access denied: {result['message']}")

# Load configuration
config_json = client.get_configuration('my-app', 'appsettings.json')

JavaScript/Node.js Integration Example

const axios = require('axios');

class ApplicationManagerClient {
    constructor(baseUrl, apiKey) {
        this.client = axios.create({
            baseURL: baseUrl,
            headers: {
                'X-Api-Key': apiKey,
                'Content-Type': 'application/json'
            }
        });
    }
    
    async getTrialUser(email) {
        try {
            const response = await this.client.get(`/api/trial-users/by-email/${email}`);
            return response.data;
        } catch (error) {
            if (error.response?.status === 404) {
                return null;
            }
            throw error;
        }
    }
    
    async validateTrial(email) {
        const user = await this.getTrialUser(email);
        
        if (!user) {
            return { valid: false, message: 'User not found' };
        }
        
        if (!user.isActive) {
            return { valid: false, message: 'User account is inactive' };
        }
        
        if (user.trial.isExpired) {
            return { 
                valid: false, 
                message: `Trial expired on ${user.trial.endDate}` 
            };
        }
        
        return {
            valid: true,
            message: 'Trial is active',
            daysRemaining: user.trial.daysRemaining
        };
    }
    
    async getConfiguration(appName, fileName) {
        const response = await this.client.get(
            `/api/configuration/${appName}/files/${fileName}`
        );
        return response.data.content;
    }
}

// Usage
const client = new ApplicationManagerClient(
    'http://localhost:11402',
    'rtk_your-api-key'
);

// Validate trial
const result = await client.validateTrial('user@example.com');
if (result.valid) {
    console.log(`Access granted. ${result.daysRemaining} days remaining.`);
} else {
    console.log(`Access denied: ${result.message}`);
}

// Load configuration
const configJson = await client.getConfiguration('my-app', 'appsettings.json');

Support

For integration support:

  • Check the API Reference for endpoint details
  • Review code examples in this guide
  • Contact: support@riptide.solutions