Skip to main content

API Documentation

API Documentation

Charon REST API documentation. All endpoints return JSON and use standard HTTP status codes.

Base URL

http://localhost:8080/api/v1

Authentication

🚧 Authentication is not yet implemented. All endpoints are currently public.

Future authentication will use JWT tokens:

Authorization: Bearer <token>

Response Format

Success Response

{
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"name": "Example",
"created_at": "2025-01-18T10:00:00Z"
}

Error Response

{
"error": "Resource not found",
"code": 404
}

Status Codes

CodeDescription
200Success
201Created
204No Content (successful deletion)
400Bad Request (validation error)
404Not Found
500Internal Server Error

Endpoints

Metrics (Prometheus)

Expose internal counters for scraping.

GET /metrics

No authentication required. Primary WAF metrics:

charon_waf_requests_total
charon_waf_blocked_total
charon_waf_monitored_total

Health Check

Check API health status.

GET /health

Response 200:

{
"status": "ok"
}

Security Suite (Cerberus)

Status

GET /security/status

Returns enabled flag plus modes for each module.

Get Global Security Config

GET /security/config

Response 200 (no config yet): { "config": null }

Upsert Global Security Config

POST /security/config
Content-Type: application/json

Request Body (example):

{
"name": "default",
"enabled": true,
"admin_whitelist": "198.51.100.10,203.0.113.0/24",
"crowdsec_mode": "local",
"waf_mode": "monitor",
"waf_rules_source": "owasp-crs-local"
}

Response 200: { "config": { ... } }

Security Considerations:

Webhook URLs configured in security settings are validated to prevent Server-Side Request Forgery (SSRF) attacks. The following destinations are blocked:

  • Private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
  • Cloud metadata endpoints (169.254.169.254)
  • Loopback addresses (127.0.0.0/8)
  • Link-local addresses

Error Response:

{
"error": "Invalid webhook URL: URL resolves to a private IP address (blocked for security)"
}

Example Valid URL:

{
"webhook_url": "https://webhook.example.com/receive"
}

Enable Cerberus

POST /security/enable

Payload (optional break-glass token):

{ "break_glass_token": "abcd1234" }

Disable Cerberus

POST /security/disable

Payload (required if not localhost):

{ "break_glass_token": "abcd1234" }

Generate Break-Glass Token

POST /security/breakglass/generate

Response 200: { "token": "plaintext-token-once" }

List Security Decisions

GET /security/decisions?limit=50

Response 200: { "decisions": [ ... ] }

Create Manual Decision

POST /security/decisions
Content-Type: application/json

Payload:

{ "ip": "203.0.113.5", "action": "block", "details": "manual temporary block" }

List Rulesets

GET /security/rulesets

Response 200: { "rulesets": [ ... ] }

Upsert Ruleset

POST /security/rulesets
Content-Type: application/json

Payload:

{
"name": "owasp-crs-quick",
"source_url": "https://example.com/owasp-crs.txt",
"mode": "owasp",
"content": "# raw rules"
}

Response 200: { "ruleset": { ... } }

Delete Ruleset

DELETE /security/rulesets/:id

Response 200: { "deleted": true }


Application URL Endpoints

Validate Application URL

Validates that a URL is properly formatted for use as the application's public URL.

POST /settings/validate-url
Content-Type: application/json
Authorization: Bearer <admin-token>

Request Body:

{
"url": "https://charon.example.com"
}

Required Fields:

  • url (string) - The URL to validate

Response 200 (Valid URL):

{
"valid": true,
"normalized": "https://charon.example.com"
}

Response 200 (Valid with Warning):

{
"valid": true,
"normalized": "http://charon.example.com",
"warning": "Using http:// instead of https:// is not recommended for production environments"
}

Response 400 (Invalid URL):

{
"valid": false,
"error": "URL must start with http:// or https:// and cannot include path components"
}

Response 403:

{
"error": "Admin access required"
}

Validation Rules:

  • URL must start with http:// or https://
  • URL cannot include path components (e.g., /admin)
  • Trailing slashes are automatically removed
  • Port numbers are allowed (e.g., :8080)
  • Warning is returned if using http:// (insecure)

Examples:

# Valid HTTPS URL
curl -X POST http://localhost:8080/api/v1/settings/validate-url \
-H "Content-Type: application/json" \
-d '{"url": "https://charon.example.com"}'

# Valid with port
curl -X POST http://localhost:8080/api/v1/settings/validate-url \
-H "Content-Type: application/json" \
-d '{"url": "https://charon.example.com:8443"}'

# Invalid - no protocol
curl -X POST http://localhost:8080/api/v1/settings/validate-url \
-H "Content-Type: application/json" \
-d '{"url": "charon.example.com"}'

# Invalid - includes path
curl -X POST http://localhost:8080/api/v1/settings/validate-url \
-H "Content-Type: application/json" \
-d '{"url": "https://charon.example.com/admin"}'

Preview User Invite URL

Generates a preview of the invite URL that would be sent to a user, without actually creating the invitation.

POST /users/preview-invite-url
Content-Type: application/json
Authorization: Bearer <admin-token>

Request Body:

{
"email": "newuser@example.com"
}

Required Fields:

  • email (string) - Email address for the preview

Response 200 (Configured):

{
"preview_url": "https://charon.example.com/accept-invite?token=SAMPLE_TOKEN_PREVIEW",
"base_url": "https://charon.example.com",
"is_configured": true,
"email": "newuser@example.com",
"warning": false,
"warning_message": ""
}

Response 200 (Not Configured):

{
"preview_url": "http://localhost:8080/accept-invite?token=SAMPLE_TOKEN_PREVIEW",
"base_url": "http://localhost:8080",
"is_configured": false,
"email": "newuser@example.com",
"warning": true,
"warning_message": "Application URL not configured. The invite link may not be accessible from external networks."
}

Response 400:

{
"error": "email is required"
}

Response 403:

{
"error": "Admin access required"
}

Field Descriptions:

  • preview_url - Complete invite URL with sample token
  • base_url - The base URL being used (configured or fallback)
  • is_configured - Whether Application URL is configured in settings
  • email - Email address from the request (echoed back)
  • warning - Boolean indicating if there's a configuration warning
  • warning_message - Human-readable warning (empty if no warning)

Use Cases:

  1. Pre-flight check: Verify invite URLs before creating users
  2. Configuration validation: Confirm Application URL is set correctly
  3. UI preview: Show users what invite link will look like
  4. Testing: Validate invite flow without creating actual invitations

Examples:

# Preview invite URL
curl -X POST http://localhost:8080/api/v1/users/preview-invite-url \
-H "Content-Type: application/json" \
-d '{"email": "admin@example.com"}'

# Response when configured:
{
"preview_url": "https://charon.example.com/accept-invite?token=SAMPLE_TOKEN_PREVIEW",
"base_url": "https://charon.example.com",
"is_configured": true,
"email": "admin@example.com",
"warning": false,
"warning_message": ""
}

JavaScript Example:

const previewInvite = async (email) => {
const response = await fetch('http://localhost:8080/api/v1/users/preview-invite-url', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer <admin-token>'
},
body: JSON.stringify({ email })
});

const data = await response.json();

if (data.warning) {
console.warn(data.warning_message);
console.log('Configure Application URL in System Settings');
} else {
console.log('Invite URL:', data.preview_url);
}
};

previewInvite('newuser@example.com');

Python Example:

import requests

def preview_invite(email, api_base='http://localhost:8080/api/v1'):
response = requests.post(
f'{api_base}/users/preview-invite-url',
headers={'Content-Type': 'application/json'},
json={'email': email}
)

data = response.json()

if data.get('warning'):
print(f"Warning: {data['warning_message']}")
else:
print(f"Invite URL: {data['preview_url']}")

return data

preview_invite('admin@example.com')

Resend User Invite

Resend an invitation email to a pending user. Generates a new invite token and sends it to the user's email address.

POST /users/:id/resend-invite
Authorization: Bearer <admin-token>

Parameters:

  • id (path) - User ID (numeric)

Response 200:

{
"email_sent": true,
"invite_url": "https://charon.example.com/accept-invite?token=abc123...",
"expires_at": "2026-01-31T12:00:00Z"
}

Response 400:

{
"error": "User is not in pending status"
}

Response 403:

{
"error": "Admin access required"
}

Response 404:

{
"error": "User not found"
}

Use Cases:

  • User didn't receive the original invitation email
  • Invite token has expired and needs renewal
  • User lost or deleted the invitation email

Example:

curl -X POST http://localhost:8080/api/v1/users/42/resend-invite \
-H "Authorization: Bearer <admin-token>"

JavaScript Example:

const resendInvite = async (userId) => {
const response = await fetch(`http://localhost:8080/api/v1/users/${userId}/resend-invite`, {
method: 'POST',
headers: {
'Authorization': 'Bearer <admin-token>'
}
});

const data = await response.json();

if (data.email_sent) {
console.log('Invitation resent successfully');
} else {
console.log('New invite created, but email could not be sent');
console.log('Invite URL:', data.invite_url);
}

return data;
};

resendInvite(42);

Test URL Connectivity

Test if a URL is reachable from the server with comprehensive SSRF (Server-Side Request Forgery) protection.

POST /settings/test-url
Content-Type: application/json
Authorization: Bearer <admin-token>

Request Body:

{
"url": "https://api.example.com"
}

Required Fields:

  • url (string) - The URL to test for connectivity

Response 200 (Reachable):

{
"reachable": true,
"latency": 145,
"message": "URL is reachable",
"error": ""
}

Response 200 (Unreachable):

{
"reachable": false,
"latency": 0,
"message": "",
"error": "connection timeout after 5s"
}

Response 400 (Invalid URL):

{
"error": "invalid URL format"
}

Response 403 (Security Block):

{
"error": "URL resolves to a private IP address (blocked for security)",
"details": "SSRF protection: private IP ranges are not allowed"
}

Response 403 (Admin Required):

{
"error": "Admin access required"
}

Field Descriptions:

  • reachable - Boolean indicating if the URL is accessible
  • latency - Response time in milliseconds (0 if unreachable)
  • message - Success message describing the result
  • error - Error message if the test failed (empty on success)

Security Features:

This endpoint implements comprehensive SSRF protection:

  1. DNS Resolution Validation - Resolves hostname with 3-second timeout
  2. Private IP Blocking - Blocks 13+ CIDR ranges:
    • RFC 1918 private networks (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
    • Loopback addresses (127.0.0.0/8, ::1/128)
    • Link-local addresses (169.254.0.0/16, fe80::/10)
    • IPv6 Unique Local Addresses (fc00::/7)
    • Multicast and other reserved ranges
  3. Cloud Metadata Protection - Blocks AWS (169.254.169.254) and GCP (metadata.google.internal) metadata endpoints
  4. Controlled HTTP Request - HEAD request with 5-second timeout
  5. Limited Redirects - Maximum 2 redirects allowed
  6. Admin-Only Access - Requires authenticated admin user

Use Cases:

  1. Webhook validation: Verify webhook endpoints before saving
  2. Application URL testing: Confirm configured URLs are reachable
  3. Integration setup: Test external service connectivity
  4. Health checks: Verify upstream service availability

Examples:

# Test a public URL
curl -X POST http://localhost:8080/api/v1/settings/test-url \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <admin-token>" \
-d '{"url": "https://api.github.com"}'

# Response:
{
"reachable": true,
"latency": 152,
"message": "URL is reachable",
"error": ""
}

# Attempt to test a private IP (blocked)
curl -X POST http://localhost:8080/api/v1/settings/test-url \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <admin-token>" \
-d '{"url": "http://192.168.1.1"}'

# Response:
{
"error": "URL resolves to a private IP address (blocked for security)",
"details": "SSRF protection: private IP ranges are not allowed"
}

JavaScript Example:

const testURL = async (url) => {
const response = await fetch('http://localhost:8080/api/v1/settings/test-url', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer <admin-token>'
},
body: JSON.stringify({ url })
});

const data = await response.json();

if (data.reachable) {
console.log(`${url} is reachable (${data.latency}ms)`);
} else {
console.error(`${url} failed: ${data.error}`);
}

return data;
};

testURL('https://api.example.com');

Python Example:

import requests

def test_url(url, api_base='http://localhost:8080/api/v1'):
response = requests.post(
f'{api_base}/settings/test-url',
headers={
'Content-Type': 'application/json',
'Authorization': 'Bearer <admin-token>'
},
json={'url': url}
)

data = response.json()

if response.status_code == 403:
print(f"Security block: {data.get('error')}")
elif data.get('reachable'):
print(f"✓ {url} is reachable ({data['latency']}ms)")
else:
print(f"✗ {url} failed: {data['error']}")

return data

test_url('https://api.github.com')

Security Considerations:

  • Only admin users can access this endpoint
  • Private IPs and cloud metadata endpoints are always blocked
  • DNS rebinding attacks are prevented by resolving before the HTTP request
  • Request timeouts prevent slowloris-style attacks
  • Limited redirects prevent redirect loops and excessive resource consumption
  • Consider rate limiting this endpoint in production environments

SSL Certificates

List All Certificates

GET /certificates

Response 200:

[
{
"id": 1,
"uuid": "cert-uuid-123",
"name": "My Custom Cert",
"provider": "custom",
"domains": "example.com, www.example.com",
"expires_at": "2026-01-01T00:00:00Z",
"created_at": "2025-01-01T10:00:00Z"
}
]

Upload Certificate

POST /certificates/upload
Content-Type: multipart/form-data

Request Body:

  • name (required) - Certificate name
  • certificate_file (required) - Certificate file (.crt or .pem)
  • key_file (required) - Private key file (.key or .pem)

Response 201:

{
"id": 1,
"uuid": "cert-uuid-123",
"name": "My Custom Cert",
"provider": "custom",
"domains": "example.com"
}

Delete Certificate

Delete a certificate. Requires that the certificate is not currently in use by any proxy hosts.

DELETE /certificates/:id

Parameters:

  • id (path) - Certificate ID (numeric)

Response 200:

{
"message": "certificate deleted"
}

Response 400:

{
"error": "invalid id"
}

Response 409:

{
"error": "certificate is in use by one or more proxy hosts"
}

Response 500:

{
"error": "failed to delete certificate"
}

Note: A backup is automatically created before deletion. The certificate files are removed from disk along with the database record.


Proxy Hosts

List All Proxy Hosts

GET /proxy-hosts

Response 200:

[
{
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"domain": "example.com, www.example.com",
"forward_scheme": "http",
"forward_host": "localhost",
"forward_port": 8080,
"ssl_forced": false,
"http2_support": true,
"hsts_enabled": false,
"hsts_subdomains": false,
"block_exploits": true,
"websocket_support": false,
"enabled": true,
"enable_standard_headers": true,
"remote_server_id": null,
"created_at": "2025-01-18T10:00:00Z",
"updated_at": "2025-01-18T10:00:00Z"
}
]

Get Proxy Host

GET /proxy-hosts/:uuid

Parameters:

  • uuid (path) - Proxy host UUID

Response 200:

{
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"domain": "example.com",
"forward_scheme": "https",
"forward_host": "backend.internal",
"forward_port": 9000,
"ssl_forced": true,
"websocket_support": false,
"enabled": true,
"enable_standard_headers": true,
"created_at": "2025-01-18T10:00:00Z",
"updated_at": "2025-01-18T10:00:00Z"
}

Response 404:

{
"error": "Proxy host not found"
}

Create Proxy Host

POST /proxy-hosts
Content-Type: application/json

Request Body:

{
"domain": "new.example.com",
"forward_scheme": "http",
"forward_host": "localhost",
"forward_port": 3000,
"ssl_forced": false,
"http2_support": true,
"hsts_enabled": false,
"hsts_subdomains": false,
"block_exploits": true,
"websocket_support": false,
"enabled": true,
"enable_standard_headers": true,
"remote_server_id": null
}

Required Fields:

  • domain - Domain name(s), comma-separated
  • forward_host - Target hostname or IP
  • forward_port - Target port number

Optional Fields:

  • forward_scheme - Default: "http"
  • ssl_forced - Default: false
  • http2_support - Default: true
  • hsts_enabled - Default: false
  • hsts_subdomains - Default: false
  • block_exploits - Default: true
  • websocket_support - Default: false
  • enabled - Default: true
  • enable_standard_headers - Default: true (for new hosts), false (for existing hosts migrated from older versions)
    • When true: Adds X-Real-IP, X-Forwarded-Proto, X-Forwarded-Host, X-Forwarded-Port headers
    • When false: Old behavior (headers only added for WebSocket or application-specific needs)
  • remote_server_id - Default: null

Response 201:

{
"uuid": "550e8400-e29b-41d4-a716-446655440001",
"domain": "new.example.com",
"forward_scheme": "http",
"forward_host": "localhost",
"enable_standard_headers": true,
"forward_port": 3000,
"created_at": "2025-01-18T10:05:00Z",
"updated_at": "2025-01-18T10:05:00Z"
}

Response 400:

{
"error": "domain is required"
}

Update Proxy Host

PUT /proxy-hosts/:uuid
Content-Type: application/json

Parameters:

  • uuid (path) - Proxy host UUID

Request Body: (all fields optional)

{
"domain": "updated.example.com",
"forward_port": 8081,
"ssl_forced": true
}

Response 200:

{
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"domain": "updated.example.com",
"forward_port": 8081,
"ssl_forced": true,
"updated_at": "2025-01-18T10:10:00Z"
}

Delete Proxy Host

DELETE /proxy-hosts/:uuid

Parameters:

  • uuid (path) - Proxy host UUID

Response 204: No content

Response 404:

{
"error": "Proxy host not found"
}

Remote Servers

List All Remote Servers

GET /remote-servers

Query Parameters:

  • enabled (optional) - Filter by enabled status (true or false)

Response 200:

[
{
"uuid": "660e8400-e29b-41d4-a716-446655440000",
"name": "Docker Registry",
"provider": "docker",
"host": "registry.local",
"port": 5000,
"reachable": true,
"last_checked": "2025-01-18T09:55:00Z",
"enabled": true,
"created_at": "2025-01-18T09:00:00Z",
"updated_at": "2025-01-18T09:55:00Z"
}
]

Get Remote Server

GET /remote-servers/:uuid

Parameters:

  • uuid (path) - Remote server UUID

Response 200:

{
"uuid": "660e8400-e29b-41d4-a716-446655440000",
"name": "Docker Registry",
"provider": "docker",
"host": "registry.local",
"port": 5000,
"reachable": true,
"enabled": true
}

Create Remote Server

POST /remote-servers
Content-Type: application/json

Request Body:

{
"name": "Production API",
"provider": "generic",
"host": "api.prod.internal",
"port": 8080,
"enabled": true
}

Required Fields:

  • name - Server name
  • host - Hostname or IP
  • port - Port number

Optional Fields:

  • provider - One of: generic, docker, kubernetes, aws, gcp, azure (default: generic)
  • enabled - Default: true

Response 201:

{
"uuid": "660e8400-e29b-41d4-a716-446655440001",
"name": "Production API",
"provider": "generic",
"host": "api.prod.internal",
"port": 8080,
"reachable": false,
"enabled": true,
"created_at": "2025-01-18T10:15:00Z"
}

Update Remote Server

PUT /remote-servers/:uuid
Content-Type: application/json

Request Body: (all fields optional)

{
"name": "Updated Name",
"port": 8081,
"enabled": false
}

Response 200:

{
"uuid": "660e8400-e29b-41d4-a716-446655440000",
"name": "Updated Name",
"port": 8081,
"enabled": false,
"updated_at": "2025-01-18T10:20:00Z"
}

Delete Remote Server

DELETE /remote-servers/:uuid

Response 204: No content

Test Remote Server Connection

Test connectivity to a remote server.

POST /remote-servers/:uuid/test

Parameters:

  • uuid (path) - Remote server UUID

Response 200:

{
"reachable": true,
"address": "registry.local:5000",
"timestamp": "2025-01-18T10:25:00Z"
}

Response 200 (unreachable):

{
"reachable": false,
"address": "offline.server:8080",
"error": "connection timeout",
"timestamp": "2025-01-18T10:25:00Z"
}

Note: This endpoint updates the reachable and last_checked fields on the remote server.


Live Logs & Notifications

Stream Live Logs (WebSocket)

Connect to a WebSocket stream of live security logs. This endpoint uses WebSocket protocol for real-time bidirectional communication.

GET /api/v1/logs/live
Upgrade: websocket

Query Parameters:

  • level (optional) - Filter by log level. Values: debug, info, warn, error
  • source (optional) - Filter by log source. Values: cerberus, waf, crowdsec, acl

WebSocket Connection:

const ws = new WebSocket('ws://localhost:8080/api/v1/logs/live?source=cerberus&level=error');

ws.onmessage = (event) => {
const logEntry = JSON.parse(event.data);
console.log(logEntry);
};

ws.onerror = (error) => {
console.error('WebSocket error:', error);
};

ws.onclose = () => {
console.log('Connection closed');
};

Message Format:

Each message received from the WebSocket is a JSON-encoded LogEntry:

{
"level": "error",
"message": "WAF blocked request from 203.0.113.42",
"timestamp": "2025-12-09T10:30:45Z",
"source": "waf",
"fields": {
"ip": "203.0.113.42",
"rule_id": "942100",
"request_uri": "/api/users?id=1' OR '1'='1",
"severity": "CRITICAL"
}
}

Field Descriptions:

  • level - Log severity: debug, info, warn, error
  • message - Human-readable log message
  • timestamp - ISO 8601 timestamp (RFC3339 format)
  • source - Component that generated the log (e.g., cerberus, waf, crowdsec)
  • fields - Additional structured data specific to the event type

Connection Lifecycle:

  • Server sends a ping every 30 seconds to keep connection alive
  • Client should respond to pings or connection may timeout
  • Server closes connection if client stops reading
  • Client can close connection by calling ws.close()

Error Handling:

  • If upgrade fails, returns HTTP 400 with error message
  • Authentication required (when auth is implemented)
  • Rate limiting applies (when rate limiting is implemented)

Example: Filter for critical WAF events only

const ws = new WebSocket('ws://localhost:8080/api/v1/logs/live?source=waf&level=error');

Get Notification Settings

Retrieve current security notification settings.

GET /api/v1/security/notifications/settings

Response 200:

{
"enabled": true,
"min_log_level": "warn",
"notify_waf_blocks": true,
"notify_acl_denials": true,
"notify_rate_limit_hits": false,
"webhook_url": "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXX",
"email_recipients": "admin@example.com,security@example.com"
}

Field Descriptions:

  • enabled - Master toggle for all notifications
  • min_log_level - Minimum severity to trigger notifications. Values: debug, info, warn, error
  • notify_waf_blocks - Send notifications for WAF blocking events
  • notify_acl_denials - Send notifications for ACL denial events
  • notify_rate_limit_hits - Send notifications for rate limit violations
  • webhook_url (optional) - URL to POST webhook notifications (Discord, Slack, etc.)
  • email_recipients (optional) - Comma-separated list of email addresses

Response 404:

{
"error": "Notification settings not configured"
}

Update Notification Settings

Update security notification settings. All fields are optional—only provided fields are updated.

PUT /api/v1/security/notifications/settings
Content-Type: application/json

Request Body:

{
"enabled": true,
"min_log_level": "error",
"notify_waf_blocks": true,
"notify_acl_denials": false,
"notify_rate_limit_hits": false,
"webhook_url": "https://discord.com/api/webhooks/123456789/abcdefgh",
"email_recipients": "alerts@example.com"
}

Security Considerations:

Webhook URLs are validated to prevent SSRF attacks. Blocked destinations:

  • Private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
  • Cloud metadata endpoints (169.254.169.254)
  • Loopback addresses (127.0.0.0/8)
  • Link-local addresses

Error Response:

{
"error": "Invalid webhook URL: URL resolves to a private IP address (blocked for security)"
}

All fields optional:

  • enabled (boolean) - Enable/disable all notifications
  • min_log_level (string) - Must be one of: debug, info, warn, error
  • notify_waf_blocks (boolean) - Toggle WAF block notifications
  • notify_acl_denials (boolean) - Toggle ACL denial notifications
  • notify_rate_limit_hits (boolean) - Toggle rate limit notifications
  • webhook_url (string) - Webhook endpoint URL
  • email_recipients (string) - Comma-separated email addresses

Response 200:

{
"message": "Settings updated successfully"
}

Response 400:

{
"error": "Invalid min_log_level. Must be one of: debug, info, warn, error"
}

Response 500:

{
"error": "Failed to update settings"
}

Example: Enable notifications for critical errors only

curl -X PUT http://localhost:8080/api/v1/security/notifications/settings \
-H "Content-Type: application/json" \
-d '{
"enabled": true,
"min_log_level": "error",
"notify_waf_blocks": true,
"webhook_url": "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
}'

Webhook Payload Format:

When notifications are triggered, Charon sends a POST request to the configured webhook URL:

{
"event_type": "waf_block",
"severity": "error",
"timestamp": "2025-12-09T10:30:45Z",
"message": "WAF blocked SQL injection attempt",
"details": {
"ip": "203.0.113.42",
"rule_id": "942100",
"request_uri": "/api/users?id=1' OR '1'='1",
"user_agent": "curl/7.68.0"
}
}

Import Workflow

Check Import Status

Check if there's an active import session.

GET /import/status

Response 200 (no session):

{
"has_pending": false
}

Response 200 (active session):

{
"has_pending": true,
"session": {
"uuid": "770e8400-e29b-41d4-a716-446655440000",
"filename": "Caddyfile",
"state": "reviewing",
"created_at": "2025-01-18T10:30:00Z",
"updated_at": "2025-01-18T10:30:00Z"
}
}

Get Import Preview

Get preview of hosts to be imported (only available when session state is reviewing).

GET /import/preview

Response 200:

{
"hosts": [
{
"domain": "example.com",
"forward_host": "localhost",
"forward_port": 8080,
"forward_scheme": "http"
},
{
"domain": "api.example.com",
"forward_host": "backend",
"forward_port": 9000,
"forward_scheme": "https"
}
],
"conflicts": [
"example.com already exists"
],
"errors": []
}

Response 404:

{
"error": "No active import session"
}

Upload Caddyfile

Upload a Caddyfile for import.

POST /import/upload
Content-Type: application/json

Request Body:

{
"content": "example.com {\n reverse_proxy localhost:8080\n}",
"filename": "Caddyfile"
}

Required Fields:

  • content - Caddyfile content

Optional Fields:

  • filename - Original filename (default: "Caddyfile")

Response 201:

{
"session": {
"uuid": "770e8400-e29b-41d4-a716-446655440000",
"filename": "Caddyfile",
"state": "parsing",
"created_at": "2025-01-18T10:35:00Z"
}
}

Response 400:

{
"error": "content is required"
}

Upload Multiple Caddyfiles

Upload multiple Caddyfiles in a single request. Useful for importing configurations from multiple site files.

POST /import/upload-multi
Content-Type: application/json

Request Body:

{
"files": [
{
"filename": "example.com.Caddyfile",
"content": "example.com {\n reverse_proxy localhost:8080\n}"
},
{
"filename": "api.example.com.Caddyfile",
"content": "api.example.com {\n reverse_proxy localhost:9000\n}"
}
]
}

Required Fields:

  • files - Array of file objects (minimum 1)
    • filename (string, required) - Original filename
    • content (string, required) - Caddyfile content

Response 200:

{
"hosts": [
{
"domain": "example.com",
"forward_host": "localhost",
"forward_port": 8080,
"forward_scheme": "http"
},
{
"domain": "api.example.com",
"forward_host": "localhost",
"forward_port": 9000,
"forward_scheme": "http"
}
],
"conflicts": [],
"errors": [],
"warning": ""
}

Response 400 (validation error):

{
"error": "files is required and must contain at least one file"
}

Response 400 (parse error with warning):

{
"error": "Caddyfile uses file_server which is not supported for import",
"warning": "file_server directive detected - static file serving is not supported"
}

TypeScript Example:

interface CaddyFile {
filename: string;
content: string;
}

const uploadCaddyfilesMulti = async (files: CaddyFile[]): Promise<ImportPreview> => {
const { data } = await client.post<ImportPreview>('/import/upload-multi', { files });
return data;
};

// Usage
const files = [
{ filename: 'site1.Caddyfile', content: 'site1.com { reverse_proxy :8080 }' },
{ filename: 'site2.Caddyfile', content: 'site2.com { reverse_proxy :9000 }' }
];
const preview = await uploadCaddyfilesMulti(files);

Commit Import

Commit the import after resolving conflicts.

POST /import/commit
Content-Type: application/json

Request Body:

{
"session_uuid": "770e8400-e29b-41d4-a716-446655440000",
"resolutions": {
"example.com": "overwrite",
"api.example.com": "keep"
}
}

Required Fields:

  • session_uuid - Active import session UUID
  • resolutions - Map of domain to resolution strategy

Resolution Strategies:

  • "keep" - Keep existing configuration, skip import
  • "overwrite" - Replace existing with imported configuration
  • "skip" - Same as keep

Response 200:

{
"imported": 2,
"skipped": 1,
"failed": 0
}

Response 400:

{
"error": "Invalid session or unresolved conflicts"
}

Cancel Import

Cancel an active import session.

DELETE /import/cancel?session_uuid=770e8400-e29b-41d4-a716-446655440000

Query Parameters:

  • session_uuid - Active import session UUID

Response 204: No content


Rate Limiting

🚧 Rate limiting is not yet implemented.

Future rate limits:

  • 100 requests per minute per IP
  • 1000 requests per hour per IP

Pagination

🚧 Pagination is not yet implemented.

Future pagination:

GET /proxy-hosts?page=1&per_page=20

Filtering and Sorting

🚧 Advanced filtering is not yet implemented.

Future filtering:

GET /proxy-hosts?enabled=true&sort=created_at&order=desc

Webhooks

🚧 Webhooks are not yet implemented.

Future webhook events:

  • proxy_host.created
  • proxy_host.updated
  • proxy_host.deleted
  • remote_server.unreachable
  • import.completed

SDKs

No official SDKs yet. The API follows REST conventions and can be used with any HTTP client.

JavaScript/TypeScript Example

const API_BASE = 'http://localhost:8080/api/v1';

// List proxy hosts
const hosts = await fetch(`${API_BASE}/proxy-hosts`).then(r => r.json());

// Create proxy host
const newHost = await fetch(`${API_BASE}/proxy-hosts`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
domain: 'example.com',
forward_host: 'localhost',
forward_port: 8080
})
}).then(r => r.json());

// Test remote server
const testResult = await fetch(`${API_BASE}/remote-servers/${uuid}/test`, {
method: 'POST'
}).then(r => r.json());

Python Example

import requests

API_BASE = 'http://localhost:8080/api/v1'

# List proxy hosts
hosts = requests.get(f'{API_BASE}/proxy-hosts').json()

# Create proxy host
new_host = requests.post(f'{API_BASE}/proxy-hosts', json={
'domain': 'example.com',
'forward_host': 'localhost',
'forward_port': 8080
}).json()

# Test remote server
test_result = requests.post(f'{API_BASE}/remote-servers/{uuid}/test').json()

Support

For API issues or questions: