The Admin API provides programmatic access to manage licenses, organizations, and system configuration. This guide covers authentication, all available endpoints, and security best practices.
Security Warning: The Admin API provides full control over your licensing system. Always protect it with:
- JWT authentication (required in production)
- IP whitelisting (configure in your reverse proxy)
- TLS/HTTPS encryption
- Strong, rotated credentials
- Enabling the Admin API
- Authentication
- Security Best Practices
- License Management
- Organization Management
- License Lifecycle
- Token Management
- Error Handling
The Admin API requires the admin-api feature flag:
# Cargo.toml
[dependencies]
talos = { git = "https://github.com/dmriding/talos", features = ["admin-api"] }For production, also enable JWT authentication:
talos = { git = "https://github.com/dmriding/talos", features = ["admin-api", "jwt-auth"] }When jwt-auth is enabled, all admin endpoints require a valid JWT token.
Configuration:
# config.toml
[auth]
enabled = true
jwt_issuer = "talos"
jwt_audience = "talos-api"
token_expiration_secs = 86400 # 24 hours# .env
TALOS_JWT_SECRET=your-secret-key-at-least-32-characters-longCreating a Token:
# Use the bootstrap token endpoint (first-time setup)
curl -X POST http://localhost:8080/api/v1/tokens \
-H "Content-Type: application/json" \
-d '{
"name": "admin-service",
"scopes": ["licenses:*", "tokens:*"]
}'Using the Token:
curl -X GET http://localhost:8080/api/v1/licenses \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Tokens are scoped to limit access:
| Scope | Description |
|---|---|
licenses:read |
Read license information |
licenses:write |
Create, update licenses |
licenses:delete |
Revoke, blacklist licenses |
licenses:* |
All license operations |
tokens:read |
Read token information |
tokens:write |
Create tokens |
tokens:delete |
Revoke tokens |
tokens:* |
All token operations |
* |
Full access (admin) |
Critical: Restrict Admin API access to trusted IP addresses.
Talos has built-in IP whitelisting that applies to all admin endpoints:
# config.toml
[admin]
# Only allow admin API access from these IPs/networks
ip_whitelist = ["127.0.0.1", "::1", "10.0.0.0/8", "192.168.0.0/16"]
audit_logging = falseFeatures:
- Supports individual IPs (
192.168.1.100) - Supports CIDR notation (
10.0.0.0/8,192.168.0.0/16) - Supports IPv4 and IPv6
- Empty list = allow all IPs (disabled)
- Checks
X-Forwarded-ForandX-Real-IPheaders when behind a proxy
Common configurations:
# Localhost only
ip_whitelist = ["127.0.0.1", "::1"]
# Internal network
ip_whitelist = ["10.0.0.0/8", "192.168.0.0/16", "172.16.0.0/12"]
# Specific servers
ip_whitelist = ["10.0.1.5", "10.0.1.6", "192.168.1.100"]For defense in depth, also configure IP restrictions at your reverse proxy:
nginx:
location /api/v1/ {
# Only allow from internal network
allow 10.0.0.0/8;
allow 192.168.0.0/16;
allow 127.0.0.1;
deny all;
proxy_pass http://talos;
}
# Client endpoints are public
location /api/v1/client/ {
proxy_pass http://talos;
}Traefik:
http:
middlewares:
admin-whitelist:
ipWhiteList:
sourceRange:
- "10.0.0.0/8"
- "192.168.0.0/16"
- "127.0.0.1"
routers:
talos-admin:
rule: "Host(`license.example.com`) && PathPrefix(`/api/v1/`) && !PathPrefix(`/api/v1/client/`)"
middlewares:
- admin-whitelist- Use HTTPS - Never expose Admin API over plain HTTP
- Rotate secrets - Change JWT secret periodically
- Audit logs - Monitor all admin actions
- Least privilege - Create tokens with minimal required scopes
- Short-lived tokens - Use short expiration times for automated systems
Creates a new license with optional organization, features, and tier.
POST /api/v1/licenses
Content-Type: application/json
Authorization: Bearer <token>
{
"org_id": "acme-corp",
"org_name": "Acme Corporation",
"tier": "pro",
"features": ["export", "api_access"],
"expires_at": "2025-12-31T23:59:59Z",
"metadata": {
"stripe_customer_id": "cus_123",
"plan": "annual"
}
}Response:
{
"license_id": "550e8400-e29b-41d4-a716-446655440000",
"license_key": "LIC-A1B2-C3D4-E5F6-G7H8",
"org_id": "acme-corp",
"org_name": "Acme Corporation",
"tier": "pro",
"features": ["export", "api_access"],
"status": "active",
"expires_at": "2025-12-31T23:59:59Z",
"created_at": "2024-01-15T10:30:00Z",
"is_bound": false
}Notes:
license_keyis auto-generated using configured prefixfeaturescan be explicit or derived fromtierconfigurationmetadatais stored as JSON and returned in responses
Create multiple licenses at once (up to 1000).
POST /api/v1/licenses/batch
Content-Type: application/json
Authorization: Bearer <token>
{
"count": 100,
"org_id": "acme-corp",
"tier": "pro",
"expires_at": "2025-12-31T23:59:59Z"
}Response:
{
"created": 100,
"licenses": [
{
"license_id": "...",
"license_key": "LIC-XXXX-YYYY-ZZZZ-AAAA"
},
...
]
}Retrieve a license by ID.
GET /api/v1/licenses/{license_id}
Authorization: Bearer <token>Response:
{
"license_id": "550e8400-e29b-41d4-a716-446655440000",
"license_key": "LIC-A1B2-C3D4-E5F6-G7H8",
"org_id": "acme-corp",
"org_name": "Acme Corporation",
"tier": "pro",
"features": ["export", "api_access"],
"status": "active",
"expires_at": "2025-12-31T23:59:59Z",
"is_bound": true,
"hardware_id": "abc123...",
"device_name": "John's Workstation",
"bound_at": "2024-01-15T14:00:00Z",
"last_seen_at": "2024-01-16T09:30:00Z"
}List licenses with filtering and pagination.
GET /api/v1/licenses?org_id=acme-corp&page=1&per_page=20
Authorization: Bearer <token>Query Parameters:
| Parameter | Description | Default |
|---|---|---|
org_id |
Filter by organization | - |
status |
Filter by status (active, suspended, revoked) | - |
page |
Page number | 1 |
per_page |
Items per page (max 100) | 20 |
Response:
{
"licenses": [...],
"total": 150,
"page": 1,
"per_page": 20,
"total_pages": 8
}Update license properties.
PATCH /api/v1/licenses/{license_id}
Content-Type: application/json
Authorization: Bearer <token>
{
"tier": "enterprise",
"features": ["export", "api_access", "sso"],
"expires_at": "2026-12-31T23:59:59Z",
"metadata": {
"upgraded_at": "2024-01-16"
}
}Notes:
- Only specified fields are updated
- Changing
tiercan auto-update features (if tier config exists)
GET /api/v1/licenses?org_id=acme-corp
Authorization: Bearer <token>You can build organization summaries using the list endpoint:
# Example: Python script to get org summary
import requests
response = requests.get(
"https://license.example.com/api/v1/licenses",
params={"org_id": "acme-corp", "per_page": 100},
headers={"Authorization": f"Bearer {token}"}
)
licenses = response.json()["licenses"]
active = sum(1 for l in licenses if l["status"] == "active")
bound = sum(1 for l in licenses if l["is_bound"])
print(f"Organization: acme-corp")
print(f"Total licenses: {len(licenses)}")
print(f"Active: {active}")
print(f"Bound: {bound}")Temporarily suspend a license (with optional grace period).
POST /api/v1/licenses/{license_id}/suspend
Content-Type: application/json
Authorization: Bearer <token>
{
"reason": "Payment failed",
"grace_period_days": 7,
"message": "Please update your payment method"
}Notes:
- Suspended licenses can still be used during grace period
- After grace period, license is automatically revoked (if background-jobs enabled)
Permanently revoke a license.
POST /api/v1/licenses/{license_id}/revoke
Content-Type: application/json
Authorization: Bearer <token>
{
"reason": "Terms of service violation"
}Notes:
- Clears hardware binding
- Cannot be used again without reinstatement
Restore a suspended or revoked license.
POST /api/v1/licenses/{license_id}/reinstate
Content-Type: application/json
Authorization: Bearer <token>
{
"new_expires_at": "2025-12-31T23:59:59Z",
"reset_bandwidth": true
}Notes:
- Cannot reinstate blacklisted licenses
- Optionally set new expiration and reset usage
Extend the expiration date.
POST /api/v1/licenses/{license_id}/extend
Content-Type: application/json
Authorization: Bearer <token>
{
"new_expires_at": "2026-12-31T23:59:59Z",
"reset_bandwidth": false
}Force-release a license from its current hardware (admin action).
POST /api/v1/licenses/{license_id}/release
Content-Type: application/json
Authorization: Bearer <token>
{
"reason": "User requested transfer to new machine"
}Notes:
- Records admin release in binding history
- License can then be bound to a different machine
Permanently ban a license (cannot be reinstated).
POST /api/v1/licenses/{license_id}/blacklist
Content-Type: application/json
Authorization: Bearer <token>
{
"reason": "License sharing detected"
}Notes:
- Sets
is_blacklisted = true - Cannot be reinstated through normal means
- Use for fraud, abuse, or policy violations
Update bandwidth/quota usage (for metered licenses).
PATCH /api/v1/licenses/{license_id}/usage
Content-Type: application/json
Authorization: Bearer <token>
{
"bandwidth_used_bytes": 1073741824,
"reset": false
}Response:
{
"license_id": "...",
"bandwidth_used_bytes": 1073741824,
"bandwidth_limit_bytes": 107374182400,
"quota_exceeded": false,
"usage_percentage": 1.0
}Create a new API token.
POST /api/v1/tokens
Content-Type: application/json
Authorization: Bearer <token>
{
"name": "billing-service",
"scopes": ["licenses:read", "licenses:write"],
"expires_in_days": 365
}Response:
{
"token_id": "tok_123",
"name": "billing-service",
"token": "eyJhbGciOiJIUzI1NiIs...",
"scopes": ["licenses:read", "licenses:write"],
"expires_at": "2025-01-15T10:30:00Z"
}Important: The token value is only shown once. Store it securely!
GET /api/v1/tokens
Authorization: Bearer <token>Response:
{
"tokens": [
{
"token_id": "tok_123",
"name": "billing-service",
"scopes": ["licenses:read", "licenses:write"],
"created_at": "2024-01-15T10:30:00Z",
"expires_at": "2025-01-15T10:30:00Z",
"last_used_at": "2024-01-16T09:00:00Z"
}
]
}DELETE /api/v1/tokens/{token_id}
Authorization: Bearer <token>All errors return a standard format:
{
"error": {
"code": "LICENSE_NOT_FOUND",
"message": "The requested license does not exist",
"details": null
}
}| Code | HTTP Status | Description |
|---|---|---|
LICENSE_NOT_FOUND |
404 | License doesn't exist |
LICENSE_EXPIRED |
403 | License has expired |
LICENSE_REVOKED |
403 | License has been revoked |
LICENSE_SUSPENDED |
403 | License is suspended |
LICENSE_BLACKLISTED |
403 | License is blacklisted |
ALREADY_BOUND |
409 | Already bound to another device |
NOT_BOUND |
409 | Not bound to any device |
INVALID_REQUEST |
400 | Request validation failed |
MISSING_FIELD |
400 | Required field missing |
MISSING_TOKEN |
401 | No auth token provided |
INVALID_TOKEN |
401 | Token is invalid |
TOKEN_EXPIRED |
401 | Token has expired |
INSUFFICIENT_SCOPE |
403 | Token lacks required scope |
DATABASE_ERROR |
500 | Database operation failed |
INTERNAL_ERROR |
500 | Unexpected server error |
import requests
response = requests.post(
"https://license.example.com/api/v1/licenses",
json={"org_id": "acme"},
headers={"Authorization": f"Bearer {token}"}
)
if not response.ok:
error = response.json().get("error", {})
code = error.get("code")
message = error.get("message")
if code == "MISSING_TOKEN":
print("Authentication required")
elif code == "INSUFFICIENT_SCOPE":
print("Token doesn't have permission for this action")
elif code == "INVALID_REQUEST":
print(f"Invalid request: {message}")
else:
print(f"Error: {message}")- Server Deployment Guide - Production deployment
- Advanced Topics - Background jobs, custom configuration
- Troubleshooting - Common issues and solutions