ExisOne API
All requests must include your access token:
Authorization: ExisOneApi <access-token>
All API endpoints enforce tokens (no request is processed without a valid token).
Public Key
GET /api/crypto/keys/public
Returns the current RSA public key in PEM format.
Activation
POST /api/license/activate
{
"activationKey": "AAAA-BBBB-CCCC-DDDD",
"email": "user@example.com",
"hardwareId": "HW-123",
"productName": "MyProduct"
}
Generate Activation Key
POST /api/activationkey/generate
{
"productName": "MyProduct",
"email": "user@example.com",
"planId": 1,
"validityDays": 365
}
- Send either
planIdorvalidityDays. If both are sent, planId overrides. - If neither is sent, the server uses the product's default license plan. Default of 0 means Perpetual (expires 2049-12-31).
Plans
GET /api/plan
POST /api/plan
{
"id": null,
"name": "365 days",
"durationDays": 365,
"isActive": true
}
Validation
Two modes are supported:
- Licensed validation (activation key present):
POST /api/license/validate
{
"activationKey": "AAAA-BBBB-CCCC-DDDD",
"hardwareId": "HW-123",
"productName": "MyProduct",
"version": "1.0"
}
Returns { isValid: true|false, status: "licensed"|"invalid", expirationDate, features: [], serverVersion?, minimumRequiredVersion? }.
Note (v0.6.0): expirationDate is now always returned, even for invalid licenses. For invalid cases, it's calculated from the device record creation date + trial days.
- Trial validation (activation key blank):
POST /api/license/validate
{
"activationKey": "",
"hardwareId": "HW-NEW",
"productName": "MyProduct",
"version": "1.0"
}
- If the device has never been activated for this product and the product has
TrialDays > 0, the server creates/updates a device record and returns{ isValid: true, status: "trial", expirationDate, remainingDays, features: [] }. - If the device was ever activated for this product, trial is not allowed and the server returns
{ isValid: false, status: "invalid", expirationDate }.
Device History
GET /api/license/device-history/{hardwareId}?page=1&pageSize=10
Returns paginated license history for a specific hardware ID in reverse chronological order.
Response: { items: [...], page: 1, pageSize: 10, totalCount: 100, totalPages: 10 }
Encrypted Payloads
Encrypt JSON with the public key (RSA-OAEP-SHA256), base64 encode, and send:
POST /api/license/validate-encrypted
{
"payload": "<base64 ciphertext>"
}
Response body is JSON; if encrypted variant is called, response will be base64 of RSA-encrypted JSON.
API Licensing
ExisOne supports API key licensing for validating your customers' API access. This allows your customers to generate API keys for their end-users with quota limits and rate limiting.
Create API License
POST /api/apilicense
{
"productId": 123,
"email": "enduser@example.com",
"clientName": "Acme Corp",
"clientIdentifier": "customer-123",
"validityDays": 365,
"maxApiCalls": 10000
}
Returns: { apiKey, expirationDate, maxApiCalls, productName, clientName, clientIdentifier }
Validate API Key
Your customers call this endpoint to validate their end-users' API requests:
POST /api/apilicense/validate
{
"apiKey": "XXXX-XXXX-XXXX-XXXX",
"units": 10,
"consumeUnits": true
}
Response:
{
"isValid": true,
"unitsConsumed": 10,
"quota": { "limit": 10000, "used": 510, "remaining": 9490 },
"rateLimits": {
"calls_minute": { "limit": 100, "used": 5, "remaining": 95, "resetsAt": "..." }
},
"features": ["api:rate:minute:100", "api:tier:premium"],
"expiresAt": "2027-01-14T23:59:59Z"
}
Check-Only Validation (No Consumption)
GET /api/apilicense/validate/{apiKey}?units=10
Returns the same response but does not consume quota units.
Get API License Details
GET /api/apilicense/{apiKey}
Get Usage Statistics
GET /api/apilicense/{apiKey}/usage
Reset Usage Counters
POST /api/apilicense/{apiKey}/reset?periodType=all
Period types: all, lifetime, minute, hour, day, month
Revoke API License
DELETE /api/apilicense/{apiKey}
Rate Limit Features
Define rate limits as product features with these naming conventions:
api:rate:minute:100- Max 100 API calls per minuteapi:rate:hour:1000- Max 1,000 calls per hourapi:rate:day:10000- Max 10,000 calls per dayapi:units:minute:500- Max 500 units consumed per minuteapi:units:hour:5000- Max 5,000 units consumed per hour
Error Codes
not_found- API key doesn't existrevoked- API key has been revokedexpired- API key has expiredquota_exhausted- Lifetime quota depletedrate_limit_minute/rate_limit_hour/rate_limit_day/rate_limit_month- Rate limit exceeded (temporary)
Response Headers
When rate limited, the response includes:
Retry-After: Seconds until the rate limit resetsX-RateLimit-Limit: The rate limit ceilingX-RateLimit-Remaining: Remaining requests in the current windowX-RateLimit-Reset: Unix timestamp when the window resets
Integration Examples
These examples show how to validate an API key from your backend when your end-user makes an API request.
Python
import requests
EXISONE_URL = "https://www.exisone.com"
ACCESS_TOKEN = "your-access-token"
def validate_api_key(api_key: str, units: int = 1) -> dict:
"""Validate an API key and consume quota units."""
response = requests.post(
f"{EXISONE_URL}/api/apilicense/validate",
headers={"Authorization": f"ExisOneApi {ACCESS_TOKEN}"},
json={"apiKey": api_key, "units": units, "consumeUnits": True}
)
return response.json()
# Example: Middleware for your API
def api_middleware():
api_key = request.headers.get("X-API-Key")
if not api_key:
return {"error": "API key required"}, 401
result = validate_api_key(api_key, units=1)
if not result.get("isValid"):
error_code = result.get("errorCode", "invalid")
if error_code == "quota_exhausted":
return {"error": "API quota exhausted"}, 429
elif error_code.startswith("rate_limit"):
retry_after = result.get("retryAfterSeconds", 60)
return {"error": "Rate limit exceeded", "retryAfter": retry_after}, 429
else:
return {"error": result.get("errorMessage", "Invalid API key")}, 401
# API key is valid - proceed with request
return None
.NET (C#)
using System.Net.Http;
using System.Text.Json;
public class ApiKeyValidator
{
private readonly HttpClient _http;
private readonly string _baseUrl = "https://www.exisone.com";
private readonly string _accessToken;
public ApiKeyValidator(string accessToken)
{
_accessToken = accessToken;
_http = new HttpClient();
_http.DefaultRequestHeaders.Add("Authorization", $"ExisOneApi {accessToken}");
}
public async Task<ApiValidationResult> ValidateAsync(string apiKey, int units = 1)
{
var response = await _http.PostAsJsonAsync(
$"{_baseUrl}/api/apilicense/validate",
new { apiKey, units, consumeUnits = true }
);
var json = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<ApiValidationResult>(json);
}
}
public record ApiValidationResult(
bool IsValid,
string? ErrorCode,
string? ErrorMessage,
int UnitsConsumed,
QuotaInfo? Quota,
int? RetryAfterSeconds
);
public record QuotaInfo(long Limit, long Used, long Remaining);
// Example: ASP.NET Core middleware
public class ApiKeyMiddleware
{
private readonly RequestDelegate _next;
private readonly ApiKeyValidator _validator;
public ApiKeyMiddleware(RequestDelegate next, ApiKeyValidator validator)
{
_next = next;
_validator = validator;
}
public async Task InvokeAsync(HttpContext context)
{
var apiKey = context.Request.Headers["X-API-Key"].FirstOrDefault();
if (string.IsNullOrEmpty(apiKey))
{
context.Response.StatusCode = 401;
await context.Response.WriteAsJsonAsync(new { error = "API key required" });
return;
}
var result = await _validator.ValidateAsync(apiKey);
if (!result.IsValid)
{
context.Response.StatusCode = result.ErrorCode?.StartsWith("rate_limit") == true ? 429 : 401;
if (result.RetryAfterSeconds.HasValue)
context.Response.Headers["Retry-After"] = result.RetryAfterSeconds.Value.ToString();
await context.Response.WriteAsJsonAsync(new { error = result.ErrorMessage });
return;
}
// Store quota info for the request if needed
context.Items["ApiQuota"] = result.Quota;
await _next(context);
}
}
JavaScript / Node.js
const EXISONE_URL = 'https://www.exisone.com';
const ACCESS_TOKEN = 'your-access-token';
async function validateApiKey(apiKey, units = 1) {
const response = await fetch(`${EXISONE_URL}/api/apilicense/validate`, {
method: 'POST',
headers: {
'Authorization': `ExisOneApi ${ACCESS_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ apiKey, units, consumeUnits: true })
});
return response.json();
}
// Example: Express.js middleware
async function apiKeyMiddleware(req, res, next) {
const apiKey = req.headers['x-api-key'];
if (!apiKey) {
return res.status(401).json({ error: 'API key required' });
}
try {
const result = await validateApiKey(apiKey, 1);
if (!result.isValid) {
const status = result.errorCode?.startsWith('rate_limit') ? 429 : 401;
if (result.retryAfterSeconds) {
res.set('Retry-After', result.retryAfterSeconds);
}
return res.status(status).json({
error: result.errorMessage,
code: result.errorCode
});
}
// Attach quota info to request for downstream use
req.apiQuota = result.quota;
req.apiFeatures = result.features;
next();
} catch (error) {
console.error('API key validation failed:', error);
return res.status(500).json({ error: 'Validation service unavailable' });
}
}
// Usage with Express
// app.use('/api', apiKeyMiddleware);
// app.get('/api/data', (req, res) => {
// console.log('Remaining quota:', req.apiQuota?.remaining);
// res.json({ data: 'your response' });
// });
Variable Unit Consumption
For APIs where different operations consume different amounts (e.g., fetching 10 records = 10 units):
// Python - consuming variable units
def fetch_records(api_key: str, count: int):
# Validate and consume `count` units
result = validate_api_key(api_key, units=count)
if not result.get("isValid"):
raise QuotaExceededError(result)
# Proceed with fetching records
return database.fetch(count)
// JavaScript - consuming variable units
app.get('/api/records', apiKeyMiddleware, async (req, res) => {
const count = parseInt(req.query.count) || 10;
// Re-validate with actual unit count
const result = await validateApiKey(req.headers['x-api-key'], count);
if (!result.isValid) {
return res.status(429).json({ error: result.errorMessage });
}
const records = await db.fetchRecords(count);
res.json({ records, remaining: result.quota?.remaining });
});