Reference
Limits, error codes, status thresholds, and the ingestion contract for Onelo Monitor. Numbers below are pulled directly from backend/app/routes/sdk_monitor.py and related migrations — keep this page bookmarked while you instrument.
Plan limits
| Limit | Free | Pro | Business |
|---|---|---|---|
| Events / hour (per app) | 1,800 | 10,000 | Unlimited |
| Storage retention | 7 days | 30 days | 90 days |
| Dashboard window | 7d | 30d | 90d |
| Monitored features per app (SDK + ping, shared pool) | 1 | 10 | Unlimited |
| Min ping interval | 15 min | 1 min | 1 min |
Hard caps (any plan)
These caps are enforced on every request regardless of plan — defense-in-depth against buggy or hostile clients.
| Cap | Value | On breach |
|---|---|---|
| Body cap | 1 MB per batch | 413 too_large |
| Events per batch | 500 max | 422 validation |
| Unique feature names per batch | 50 max | 422 validation |
| error string | ≤ 8 KB | 422 validation |
| meta serialized JSON | ≤ 16 KB | 422 validation |
| meta nesting depth | ≤ 8 | 422 validation |
| userId | ≤ 256 chars | 422 validation |
| platform | ≤ 32 chars (also closed-list coerced) | Coerced to "unknown" |
| source | ≤ 64 chars (also closed-list coerced) | Coerced to "event" |
| sessionId | ≤ 128 chars | 422 validation |
| durationMs | ≤ 3,600,000 (1 hour) | 422 validation |
| featureName | ≤ 200 chars | 422 validation |
| meta.calls (feature_call_summary) | ≤ 50,000 per summary event | Clamped |
| Aggregates / facets row cap | 50,000 rows in window | 409 window_too_wide |
| Export row cap | 10,000 rows | 409 too_large |
| Time range max | 365 days | 403 retention_exceeded |
HTTP error codes
| Code | Meaning | Action |
|---|---|---|
| 204 | Empty batch (no events to process). | OK — no-op. |
| 422 | Validation error (missing or invalid field, or a cap above breached). | Fix the payload; check the field caps above. |
| 401 | Invalid publishableKey. | Verify the key against the dashboard. |
| 403 | Origin not allowed (web only); plan retention exceeded; bundle id not registered; etc. | Inspect the response detail field. For web, add the page origin to allowed_origins in the dashboard. |
| 409 | Result too large (export > 10k, aggregates > 50k). | Narrow filters or shorten the time range. |
| 413 | Body > 1 MB. | Reduce batch size — trim meta, or flush more often (batches cap at 500 events). |
| 429 | Per-IP slowapi exhaust OR per-app hourly quota exhausted. | Honor Retry-After and X-Onelo-Quota-Remaining. |
| 503 | origin_check_unavailable — the origin allowlist could not be read (web only). | Transient — retry shortly. |
Quota headers
Non-empty ingest responses include the headers below so SDKs and operators can self-throttle (an empty batch returns a bare 204 without them).
| Header | Description |
|---|---|
| X-Onelo-Quota-Limit | Events/hour for this plan — the literal string "unlimited" when the plan has no cap. |
| X-Onelo-Quota-Remaining | Events left in the current hour window. |
| Retry-After | Only set on 429 responses. A fixed backoff hint: 60 s for the per-IP limit, 3600 s for the hourly quota — not a live countdown to the window reset. |
To shed load before you hit the hourly quota, the Python SDK samples client-side: monitor.init(sample_rate=…, success_sample_rate=…) — keep-probabilities for error vs success/track events (default 1.0 = keep everything). A high-throughput backend can drop the noisy success events while keeping every error.
Status thresholds (Feature Health)
Source: backend/app/routes/monitor_mgmt.py. Thresholds are per-feature and configurable via the alert rule — the values below use the default of 0.10.
| Condition | Status |
|---|---|
| total_calls == 0 in window | no_data |
| total_calls < 10 | low_data |
| errorRate ≥ threshold (default 0.10) | down |
| errorRate ≥ threshold × 0.5 (default 0.05) | degraded |
| errorRate < threshold × 0.5 | ok |
HTTP ping monitors use a different rule (same file): status is taken from the most recent ping and is only ever no_data, degraded, or ok — there is no down state. It reads degraded when the last ping failed or the error rate over the last 200 checks is ≥ 0.10.
Closed-list field values
Both platform and source are closed lists. Values outside the list are silently coerced to a sentinel — this prevents a malicious or buggy client from inflating another platform's counters in dashboards or billing.
| Field | Allowed values | Coercion sentinel |
|---|---|---|
| platform | swift, python, js, electron, reactnative, android, flutter, kotlin, node, php | unknown |
| source | feature_call, feature_call_summary, track, event, global_error | event |
Wire format
The single ingestion endpoint. The SDK's apiUrl is your Onelo API root.
POST {apiUrl}/api/sdk/monitor/events/batch
Content-Type: application/json{
"publishableKey": "onelo_pk_live_…",
"events": [
{
"featureName": "checkout.charge",
"ok": false,
"durationMs": 1342,
"error": "Stripe declined: insufficient_funds",
"meta": { "amount": 1999 },
"source": "track",
"userId": "usr_abc",
"platform": "swift",
"sessionId": "sess_2026_05_10_42"
}
]
}The publishableKey field carries your app key. Client SDKs send the publishable key (onelo_pk_live_*); the backend SDKs (Python, Node.js, PHP) send their server secret key (onelo_sk_live_*) in this same field. The endpoint accepts either.
Two behaviours worth knowing: an optional client-supplied event timestamp is display-only and ignored when it is older than 1 hour or more than 60 seconds in the future (the row falls back to server receive time — quotas, windows and incidents always key on server time). And key lookups are cached for up to 60 seconds, so a revoked key can keep authenticating for up to a minute after revocation.
REST API (dashboard read)
All endpoints below require Authorization: Bearer <session_token> (the dashboard JWT). They are not part of the public SDK contract.
| Method · Path | Description |
|---|---|
| GET /api/monitor/features?app_id=… | Feature Health: one row per feature, with status and aggregates over the selected window (range=1h|24h|7d|30d or from/to; response fields are named calls24h / users24h / sessions24h regardless of the window). |
| POST /api/monitor/features | Add an HTTP ping monitor. |
| DELETE /api/monitor/features/{feature_id} | Remove a monitor. |
| GET /api/monitor/features/{feature_name}/runs?app_id=… | Runs list with filters (status, platform, environment, release, q, …) and cursor pagination. |
| GET /api/monitor/features/{feature_name}/logs?app_id=… | Raw event list over the selected window (range or from/to only — no filters). |
| GET /api/monitor/features/{feature_name}/aggregates?app_id=…&group_by=… | Aggregated runs along one facet. |
| GET /api/monitor/features/{feature_name}/facets?app_id=… | Facet value counts for the filter dropdowns. |
| GET /api/monitor/features/{feature_name}/runs/export?app_id=…&format=csv|json | Streaming export (same filters as /runs; capped at 10,000 rows). |
| GET /api/monitor/features/{feature_name}/alert-rule?app_id=… | Read the per-feature alert rule. |
| PATCH /api/monitor/features/{feature_name}/alert-rule | Update threshold / window / enabled for a feature. |
| GET /api/monitor/issues?app_id=… | Grouped error issues. |
| GET /api/monitor/issues/{issue_id}/samples | Sample events for one issue. |
| GET /api/monitor/sessions/{session_id}/events | All events for a single session — used by the run drawer’s “Open session timeline” button. |
| GET /api/monitor/incidents?app_id=… | List incidents for an app. |
| GET /api/monitor/incidents/{id} | Fetch a single incident. |
| PATCH /api/monitor/incidents/{id} | Resolve, change priority, or edit description / notes. |
| POST /api/monitor/incidents/{id}/notify | Re-send the alert email for an incident. |
| GET /api/monitor/alert-config?app_id=… | Read per-app alert email configuration. |
| PUT /api/monitor/alert-config | Save per-app alert email configuration. |
| POST | GET /api/monitor/credentials · DELETE /api/monitor/credentials/{id} | Manage Monitor Credentials for authenticated HTTP pings. |
| GET /api/monitor/notifications | In-app bell-icon notifications. |
| PATCH /api/monitor/notifications/{id}/read | Mark a notification as read. |
| POST /api/monitor/notifications/read-all | Mark all notifications as read. |
{feature_name} (the feature's name, not its UUID). Only DELETE /features/{feature_id} takes the UUID.