REST API Guide
Who is this for? This guide is for developers who are not using the KMP SDK — for example, teams building Python, Node.js, Go, Ruby, PHP, or plain HTTP clients. Every feature available in the SDK is also available directly over HTTP.
Base URL
https://onemanage.quest
All endpoints are served over HTTPS. Substitute your self-hosted domain if you are running a private OneManage instance.
Authentication
OneManage uses two independent authentication schemes depending on the type of endpoint.
1. API Key (for SDK-style ingestion endpoints)
Used by your application (mobile app, backend service) to send data to OneManage. Credentials are passed as HTTP request headers — never in the URL.
| Header | Description |
|---|---|
X-API-KEY |
Your account's API key (starts with sk-live-) |
X-APP-ID |
The identifier of the registered app sending data |
Both headers are required for all ingestion endpoints.
Get your credentials: Log in to the OneManage Dashboard, navigate to Settings → API Key and Apps to find your App ID.
Example (curl)
curl -X POST https://onemanage.quest/logs/ingest/app \
-H "Content-Type: application/json" \
-H "X-API-KEY: sk-live-xxxxxxxxxxxx" \
-H "X-APP-ID: my-android-app" \
-d '{ ... }'
2. JWT Cookie (for dashboard / management endpoints)
Dashboard management endpoints (e.g. listing configs, resolving bugs) are protected by a JWT session cookie issued at login. These endpoints are intended for dashboard access and are not designed for programmatic API consumers in external integrations.
Note: For machine-to-machine use, stick to the API Key–protected ingestion endpoints documented in this guide. Management endpoints may require interactive login and are subject to change.
Response Format
All API responses are wrapped in a standard envelope:
Success
{
"statusCode": 200,
"message": "Logs ingested successfully",
"data": { ... }
}
Failure
{
"statusCode": 401,
"message": "Missing API Key",
"data": null
}
| Field | Type | Description |
|---|---|---|
statusCode |
Int |
HTTP status code mirrored in the body |
message |
String |
Human-readable status message |
data |
Object | null |
Response payload, or null on error |
Endpoints
1. Log Ingestion
1.1 Ingest App Logs
Use this endpoint for mobile or frontend apps (Android, iOS, Desktop). It accepts enriched device and user context alongside the log batch.
POST /logs/ingest/app
Headers:
| Header | Required | Description |
|---|---|---|
Content-Type |
✅ | application/json |
X-API-KEY |
✅ | Your account API key |
X-APP-ID |
✅ | The app identifier |
Request Body:
{
"schemaVersion": "1.0",
"source": "APP",
"batchId": "batch_a1b2c3d4",
"packageName": "com.example.myapp",
"appVersion": "2.1.0",
"environment": "production",
"userInfo": {
"userId": "user_123456",
"isAnonymous": false
},
"deviceInfo": {
"model": "Pixel 8",
"manufacturer": "Google",
"osName": "Android",
"osVersion": "14",
"locale": "en-US",
"country": "US"
},
"logs": [
{
"logId": "log_unique_id_001",
"level": "INFO",
"logger": "HomeScreen",
"timestamp": 1750000000000,
"thread": "main",
"event": "screen_opened",
"message": "User opened the home screen",
"stacktrace": null,
"errorCode": null,
"metadata": {
"referrer": "splash_screen"
}
},
{
"logId": "log_unique_id_002",
"level": "ERROR",
"logger": "CheckoutService",
"timestamp": 1750000001000,
"thread": "io-pool-1",
"event": "payment_failed",
"message": "Payment timed out after 30s",
"stacktrace": "java.net.SocketTimeoutException: timeout\n\tat ...",
"errorCode": "ERR_TIMEOUT",
"metadata": {
"cart_total": "149.99",
"currency": "USD"
}
}
]
}
Field Reference — Top Level:
| Field | Type | Required | Description |
|---|---|---|---|
schemaVersion |
String |
✅ | Schema version — use "1.0" |
source |
String |
✅ | Must be "APP" |
batchId |
String |
❌ | Unique ID for this batch, used for deduplication |
packageName |
String |
✅ | App package/bundle identifier (e.g. com.example.app) |
appVersion |
String |
✅ | App version string (e.g. "2.1.0") |
environment |
String |
✅ | "production", "staging", or "debug" |
userInfo |
Object |
✅ | See userInfo schema below |
deviceInfo |
Object |
✅ | See deviceInfo schema below |
logs |
Array<Log> |
✅ | One or more log entries. Max 50 per batch |
userInfo Object:
| Field | Type | Required | Description |
|---|---|---|---|
userId |
String? |
❌ | Authenticated user ID, or null for anonymous |
isAnonymous |
Boolean |
✅ | true if no authenticated user is present |
deviceInfo Object:
| Field | Type | Required | Description |
|---|---|---|---|
model |
String? |
❌ | Device model name (e.g. "Pixel 8", "iPhone 15") |
manufacturer |
String? |
❌ | Device manufacturer (e.g. "Google", "Apple") |
osName |
String |
✅ | Operating system name (e.g. "Android", "iOS") |
osVersion |
String |
✅ | OS version string (e.g. "14", "17.2") |
locale |
String? |
❌ | BCP-47 locale tag (e.g. "en-US") |
country |
String? |
❌ | ISO 3166-1 alpha-2 country code (e.g. "US") |
logs[] Object:
| Field | Type | Required | Description |
|---|---|---|---|
logId |
String |
✅ | Unique ID for this log entry (use UUID v4) |
level |
String |
✅ | "DEBUG", "INFO", "WARN", "ERROR" |
logger |
String? |
❌ | Source tag / logger name (e.g. "HomeScreen") |
timestamp |
Long |
✅ | Unix epoch in milliseconds |
thread |
String? |
❌ | Thread name at time of logging |
event |
String |
✅ | Machine-readable event key (e.g. "payment_failed") |
message |
String |
✅ | Human-readable log message |
stacktrace |
String? |
❌ | Full stack trace string for errors |
errorCode |
String? |
❌ | Application-defined error code (e.g. "ERR_TIMEOUT") |
metadata |
Object |
❌ | Flat String → String map for extra context |
Success Response (200 OK):
{
"statusCode": 200,
"message": "Logs ingested successfully",
"data": null
}
Error Responses:
| Status | Reason |
|---|---|
401 Unauthorized |
Missing or invalid X-API-KEY |
400 Bad Request |
Malformed JSON body |
500 Internal Server Error |
Server-side failure |
1.2 Ingest Server / Backend Logs
Use this endpoint for backend services (Node.js, Python, Go, Java, etc.). Provides a runtime context block instead of device info.
POST /logs/ingest/server
Headers: Same as App Logs (Content-Type, X-API-KEY, X-APP-ID).
Request Body:
{
"schemaVersion": "1.0",
"source": "SERVER",
"batchId": "batch_srv_xyz789",
"serviceName": "user-service",
"serviceVersion": "3.4.1",
"environment": "production",
"runtime": {
"osName": "Linux",
"osVersion": "5.15.0",
"jvmVersion": "21.0.2"
},
"logs": [
{
"logId": "srv_log_001",
"level": "INFO",
"logger": "UserController",
"timestamp": 1750000000000,
"thread": "ktor-worker-1",
"event": "user_registered",
"message": "New user registered: user@example.com",
"stacktrace": null,
"errorCode": null,
"metadata": {
"plan": "free"
},
"requestInfo": null
},
{
"logId": "srv_log_002",
"level": "ERROR",
"logger": "PaymentService",
"timestamp": 1750000005000,
"thread": "ktor-worker-3",
"event": "payment_gateway_error",
"message": "Stripe webhook validation failed",
"stacktrace": "com.stripe.StripeException: ...",
"errorCode": "STRIPE_SIG_INVALID",
"metadata": {},
"requestInfo": {
"requestId": "req_abc123",
"method": "POST",
"path": "/webhooks/stripe",
"statusCode": 400,
"durationMs": 12
}
}
]
}
Field Reference — Top Level (differences from App Logs):
| Field | Type | Required | Description |
|---|---|---|---|
source |
String |
✅ | Must be "SERVER" |
serviceName |
String |
✅ | Name of your backend service (e.g. "user-service") |
serviceVersion |
String |
✅ | Version of your backend service |
runtime |
Object |
✅ | See runtime schema below |
runtime Object:
| Field | Type | Required | Description |
|---|---|---|---|
osName |
String |
✅ | Host OS (e.g. "Linux", "Windows") |
osVersion |
String |
✅ | OS version string |
jvmVersion |
String |
✅ | Runtime version (use "N/A" for non-JVM runtimes) |
logs[].requestInfo Object (optional — attach to logs generated during an HTTP request):
| Field | Type | Required | Description |
|---|---|---|---|
requestId |
String |
✅ | Unique request/trace ID |
method |
String |
✅ | HTTP method ("GET", "POST", etc.) |
path |
String |
✅ | Request path (e.g. "/api/users") |
statusCode |
Int |
✅ | HTTP response status code |
durationMs |
Long |
✅ | Request duration in milliseconds |
Success Response (200 OK): Same as App Logs.
Code Examples — Log Ingestion
Node.js (fetch)
const BASE_URL = 'https://onemanage.quest';
const API_KEY = 'sk-live-xxxxxxxxxxxx';
const APP_ID = 'my-node-service';
async function sendLogs(logs) {
const batch = {
schemaVersion: '1.0',
source: 'SERVER',
batchId: `batch_${Date.now()}`,
serviceName: 'my-node-service',
serviceVersion: '1.0.0',
environment: process.env.NODE_ENV ?? 'production',
runtime: {
osName: process.platform,
osVersion: process.version,
jvmVersion: 'N/A'
},
logs: logs.map(l => ({
logId: crypto.randomUUID(),
level: l.level,
logger: l.tag,
timestamp: Date.now(),
event: l.event,
message: l.message,
metadata: l.metadata ?? {}
}))
};
const res = await fetch(`${BASE_URL}/logs/ingest/server`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-KEY': API_KEY,
'X-APP-ID': APP_ID
},
body: JSON.stringify(batch)
});
if (!res.ok) {
console.error('OneManage ingest failed:', await res.text());
}
}
Python (requests)
import requests, uuid, time, platform
BASE_URL = "https://onemanage.quest"
API_KEY = "sk-live-xxxxxxxxxxxx"
APP_ID = "my-python-service"
def send_logs(entries: list[dict]):
batch = {
"schemaVersion": "1.0",
"source": "SERVER",
"batchId": f"batch_{uuid.uuid4().hex[:8]}",
"serviceName": "my-python-service",
"serviceVersion": "1.0.0",
"environment": "production",
"runtime": {
"osName": platform.system(),
"osVersion": platform.release(),
"jvmVersion": "N/A"
},
"logs": [
{
"logId": str(uuid.uuid4()),
"level": entry.get("level", "INFO"),
"logger": entry.get("tag", "app"),
"timestamp": int(time.time() * 1000),
"event": entry.get("event", "log"),
"message": entry["message"],
"metadata": entry.get("metadata", {})
}
for entry in entries
]
}
resp = requests.post(
f"{BASE_URL}/logs/ingest/server",
json=batch,
headers={
"X-API-KEY": API_KEY,
"X-APP-ID": APP_ID
},
timeout=10
)
resp.raise_for_status()
Go
package onemanage
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"runtime"
"time"
"github.com/google/uuid"
)
const (
baseURL = "https://onemanage.quest"
apiKey = "sk-live-xxxxxxxxxxxx"
appID = "my-go-service"
)
type LogEntry struct {
LogID string `json:"logId"`
Level string `json:"level"`
Logger string `json:"logger"`
Timestamp int64 `json:"timestamp"`
Event string `json:"event"`
Message string `json:"message"`
Metadata map[string]string `json:"metadata"`
}
type Batch struct {
SchemaVersion string `json:"schemaVersion"`
Source string `json:"source"`
BatchID string `json:"batchId"`
ServiceName string `json:"serviceName"`
ServiceVersion string `json:"serviceVersion"`
Environment string `json:"environment"`
Runtime Runtime `json:"runtime"`
Logs []LogEntry `json:"logs"`
}
type Runtime struct {
OsName string `json:"osName"`
OsVersion string `json:"osVersion"`
JvmVersion string `json:"jvmVersion"`
}
func SendLog(level, event, message string) error {
batch := Batch{
SchemaVersion: "1.0",
Source: "SERVER",
BatchID: uuid.New().String(),
ServiceName: "my-go-service",
ServiceVersion: "1.0.0",
Environment: "production",
Runtime: Runtime{
OsName: runtime.GOOS,
OsVersion: "unknown",
JvmVersion: "N/A",
},
Logs: []LogEntry{
{
LogID: uuid.New().String(),
Level: level,
Logger: "app",
Timestamp: time.Now().UnixMilli(),
Event: event,
Message: message,
Metadata: map[string]string{},
},
},
}
body, _ := json.Marshal(batch)
req, _ := http.NewRequest("POST", baseURL+"/logs/ingest/server", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-API-KEY", apiKey)
req.Header.Set("X-APP-ID", appID)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return fmt.Errorf("onemanage: ingest failed with status %d", resp.StatusCode)
}
return nil
}
2. Bug Reports
2.1 Submit a Bug Report
Submit a structured bug report to your OneManage Dashboard from any environment or language.
POST /bugs/ingest
Headers:
| Header | Required | Description |
|---|---|---|
Content-Type |
✅ | application/json |
X-API-KEY |
✅ | Your account API key |
X-APP-ID |
✅ | The app identifier |
Request Body:
{
"identifier": "com.example.myapp",
"title": "Crash on checkout",
"description": "App crashes when the user taps 'Pay Now' with an expired card.",
"severity": "HIGH",
"steps": "1. Add item to cart\n2. Go to checkout\n3. Enter expired card\n4. Tap 'Pay Now'\n5. Observe crash",
"appVersion": "2.1.0",
"submittedBy": "user_123456",
"metadata": {
"screen": "CheckoutScreen",
"network_type": "WiFi",
"os": "Android 14"
}
}
Field Reference:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
identifier |
String? |
❌ | null |
App package/bundle name (helps resolve the app on the server) |
title |
String |
✅ | — | Short, descriptive bug title |
description |
String |
✅ | — | Detailed description of the issue |
severity |
String |
❌ | "NORMAL" |
"LOW", "NORMAL", "HIGH", "CRITICAL" |
steps |
String? |
❌ | null |
Steps to reproduce (plain text, newlines accepted) |
appVersion |
String? |
❌ | null |
App version when the bug occurred |
submittedBy |
String? |
❌ | null |
User ID or email of the person reporting the bug |
metadata |
Object |
❌ | null |
Flat String → String map for extra device/context info |
Severity Levels:
| Value | When to use |
|---|---|
"LOW" |
Cosmetic issues, minor visual glitches |
"NORMAL" |
Standard bugs affecting user experience |
"HIGH" |
Feature broken for a segment of users |
"CRITICAL" |
App crash, data loss, or security vulnerability |
Success Response (200 OK):
{
"statusCode": 200,
"message": "Bug report submitted successfully",
"data": null
}
Code Examples — Bug Reports
Node.js
async function submitBug({ title, description, severity = 'NORMAL', steps, metadata }) {
await fetch('https://onemanage.quest/bugs/ingest', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-KEY': 'sk-live-xxxxxxxxxxxx',
'X-APP-ID': 'my-node-service'
},
body: JSON.stringify({ title, description, severity, steps, metadata })
});
}
// Usage
await submitBug({
title: 'DB connection pool exhausted',
description: 'All connections in the pool are busy under sustained load.',
severity: 'CRITICAL',
metadata: { pool_size: '10', active_connections: '10' }
});
Python
import requests
def submit_bug(title: str, description: str, severity: str = "NORMAL", **kwargs):
requests.post(
"https://onemanage.quest/bugs/ingest",
json={"title": title, "description": description, "severity": severity, **kwargs},
headers={
"X-API-KEY": "sk-live-xxxxxxxxxxxx",
"X-APP-ID": "my-python-service"
},
timeout=10
).raise_for_status()
# Usage
submit_bug(
title="Redis cache miss storm",
description="Cache miss rate spiked to 100% after deployment.",
severity="HIGH",
metadata={"cache_hit_rate": "0%", "deployment_id": "d-abc123"}
)
3. Remote Config
3.1 Fetch Remote Config
Retrieve all active configuration values for a specific app. The response returns a flat key→value map ready for immediate use in your application.
GET /config/fetch
Headers:
| Header | Required | Description |
|---|---|---|
X-API-KEY |
✅ | Your account API key |
X-APP-ID |
✅ | The app identifier |
No request body required.
Success Response (200 OK):
{
"statusCode": 200,
"message": "Config fetched successfully",
"data": {
"configs": {
"dark_mode_enabled": true,
"welcome_message": "Hello, world!",
"max_retry_count": 3,
"discount_rate": 0.15,
"session_timeout_ms": 300000
},
"fetchedAt": 1750000000000
}
}
Response data Fields:
| Field | Type | Description |
|---|---|---|
configs |
Object |
Flat map of key → typed value. Values are typed (boolean, number, string) based on the type configured in your dashboard. |
fetchedAt |
Long |
Unix epoch (ms) when the config was fetched |
Tip: You should cache this response locally and re-fetch on app startup or periodically (e.g. every 30 minutes). Avoid calling this on every request.
Code Examples — Remote Config
Node.js
let cachedConfig = {};
async function fetchRemoteConfig() {
const res = await fetch('https://onemanage.quest/config/fetch', {
headers: {
'X-API-KEY': 'sk-live-xxxxxxxxxxxx',
'X-APP-ID': 'my-node-service'
}
});
const body = await res.json();
if (body.statusCode === 200) {
cachedConfig = body.data.configs;
console.log('Config refreshed at', new Date(body.data.fetchedAt));
}
}
function getConfig(key, defaultValue) {
return cachedConfig[key] ?? defaultValue;
}
// On startup
await fetchRemoteConfig();
// Usage
const isFeatureEnabled = getConfig('new_checkout_flow', false);
const maxRetries = getConfig('max_retry_count', 3);
Python
import requests, time
_config_cache = {}
_cache_ts = 0
CACHE_TTL_SECONDS = 1800 # 30 minutes
def fetch_remote_config():
global _config_cache, _cache_ts
if time.time() - _cache_ts < CACHE_TTL_SECONDS:
return # Still fresh
resp = requests.get(
"https://onemanage.quest/config/fetch",
headers={
"X-API-KEY": "sk-live-xxxxxxxxxxxx",
"X-APP-ID": "my-python-service"
},
timeout=10
)
resp.raise_for_status()
body = resp.json()
if body["statusCode"] == 200:
_config_cache = body["data"]["configs"]
_cache_ts = time.time()
def get_config(key: str, default=None):
fetch_remote_config()
return _config_cache.get(key, default)
# Usage
is_enabled = get_config("new_checkout_flow", False)
max_retries = get_config("max_retry_count", 3)
Log Levels Reference
| Level | String Value | When to Use |
|---|---|---|
| Debug | "DEBUG" |
Verbose diagnostic info — suppress in production |
| Info | "INFO" |
Normal app lifecycle events |
| Warning | "WARN" |
Recoverable issues, unexpected states |
| Error | "ERROR" |
Errors that affect user experience |
Batching Best Practices
The OneManage ingestion API is optimized for batched writes. Sending individual logs one at a time is wasteful and can trigger rate limits.
Recommended approach:
- Buffer logs in-memory up to a maximum of 50 entries or a time window of 15 seconds (whichever comes first).
- Send the batch to
/logs/ingest/appor/logs/ingest/serveras a single HTTP request. - On failure, store the batch locally and retry with exponential backoff (1s, 2s, 4s, max 3 retries).
- On app shutdown, flush any remaining buffered logs immediately.
// Minimal Node.js batcher
class OneManageBatcher {
#buffer = [];
#timer = null;
constructor(flushIntervalMs = 15000, maxBatchSize = 50) {
this.flushIntervalMs = flushIntervalMs;
this.maxBatchSize = maxBatchSize;
}
log(level, event, message, metadata = {}) {
this.#buffer.push({
logId: crypto.randomUUID(),
level,
event,
message,
metadata,
timestamp: Date.now()
});
if (this.#buffer.length >= this.maxBatchSize) {
this.flush();
} else if (!this.#timer) {
this.#timer = setTimeout(() => this.flush(), this.flushIntervalMs);
}
}
async flush() {
if (this.#timer) { clearTimeout(this.#timer); this.#timer = null; }
if (this.#buffer.length === 0) return;
const batch = this.#buffer.splice(0);
// Build and POST the batch payload here (see Node.js example above)
await sendLogs(batch);
}
info(event, msg, meta) { this.log('INFO', event, msg, meta); }
warn(event, msg, meta) { this.log('WARN', event, msg, meta); }
error(event, msg, meta) { this.log('ERROR', event, msg, meta); }
debug(event, msg, meta) { this.log('DEBUG', event, msg, meta); }
}
// Usage
const logger = new OneManageBatcher();
process.on('exit', () => logger.flush());
logger.info('app_started', 'Application started');
logger.error('db_error', 'Connection refused', { host: 'db.internal' });
Rate Limits
| Endpoint | Limit |
|---|---|
/logs/ingest/app |
60 requests / minute per API key |
/logs/ingest/server |
60 requests / minute per API key |
/bugs/ingest |
30 requests / minute per API key |
/config/fetch |
20 requests / minute per API key |
Exceeding the rate limit returns a
429 Too Many Requestsresponse. Back off and retry after 60 seconds.
Error Codes Reference
| HTTP Status | Meaning | Resolution |
|---|---|---|
400 Bad Request |
Malformed JSON or missing required fields | Check your request body against the schema |
401 Unauthorized |
Missing or invalid X-API-KEY |
Verify your API key in the dashboard |
400 Bad Request |
Missing X-APP-ID |
Include the X-APP-ID header |
404 Not Found |
App ID not found in your account | Verify the App ID exists in the dashboard |
429 Too Many Requests |
Rate limit exceeded | Implement exponential backoff |
500 Internal Server Error |
Server-side failure | Retry later; contact support if persistent |
SDK vs REST API — Feature Comparison
| Feature | KMP SDK | REST API |
|---|---|---|
| App Log Ingestion | ✅ Automatic batching & retry | ✅ Manual batching required |
| Server Log Ingestion | ✅ Ktor SDK plugin | ✅ Any HTTP client |
| Bug Report Submission | ✅ Auto device metadata | ✅ Manual metadata |
| Remote Config | ✅ SQLDelight cache + StateFlow | ✅ Manual caching |
| Offline Persistence | ✅ Built-in | ❌ Must implement yourself |
| Auto User Identity | ✅ SDK-managed UUID | ❌ Must generate/track yourself |
| Platform Support | Android, iOS, JVM | Any language or platform |
Support & Resources
- Dashboard: onemanage.quest
- KMP SDK Docs: Install the SDK →
- Sample App: iammohdzaki/onemanage-app-sample
- Issues & Questions: Open an issue on the sample repository