onairos
Version:
The Onairos Library is a collection of functions that enable Applications to connect and communicate data with Onairos Identities via User Authorization. Integration for developers is seamless, simple and effective for all applications. LLM SDK capabiliti
525 lines (406 loc) • 15.3 kB
Markdown
**Version:** 2026-04-24
**Status:** Live
**Training status URL:** Prefer **`GET https://api2.onairos.uk/training/status`** (Bearer token). Legacy alias **`/mobile-training/status`** is identical. With `environment: 'development'`, the SDK uses **`https://dev-api.onairos.uk`** as the API base; the backend’s `training.statusUrl` in the authorize response is authoritative—always poll that URL if present.
---
This document outlines the API response changes and new background training feature for developers integrating with Onairos.
---
**OLD Format:**
```json
{
"apiUrl": "https://api2.onairos.uk/combinedInference",
"token": "eyJ...",
"userData": {
"email": "user@example.com",
"username": "johndoe",
"connectedPlatforms": ["linkedin", "youtube"],
"isReturningUser": true
}
}
```
**NEW Format:**
```json
{
"apiUrl": "https://api2.onairos.uk/combinedInference",
"token": "eyJ...",
"authorizedData": {
"traits": true,
"personality": true,
"llmData": false
},
"training": {
"ready": true,
"statusUrl": "https://api2.onairos.uk/training/status",
"pollIntervalMs": 5000
}
}
```
| Old Field | New Field | Notes |
|-----------|-----------|-------|
| `userData.email` | **REMOVED** | Security: No PII exposed |
| `userData.username` | **REMOVED** | Security: No PII exposed |
| `userData.connectedPlatforms` | **REMOVED** | Available in traits response |
| `userData.isReturningUser` | **REMOVED** | Use `training.ready` instead |
| *(new)* | `authorizedData` | What data types user authorized |
| *(new)* | `training.ready` | `true` = fetch now, `false` = poll first |
| *(new)* | `training.statusUrl` | Endpoint to check training progress |
| *(new)* | `training.pollIntervalMs` | Recommended polling interval |
---
**NEW Format:**
```json
{
"InferenceResult": { "output": [[0.8, 0.2]] },
"traits": {
"positive_traits": { "Curious": 92, "Analytical": 88 },
"traits_to_improve": { "Patience": 45 },
"user_summary": "You're a thoughtful technologist...",
"top_traits_explanation": "Your engagement patterns show...",
"archetype": "Strategic Thinker",
"nudges": [
{ "text": "Try journaling a decision you're mulling over" }
]
},
"userProfile": {
"user_summary": "You're a thoughtful technologist...",
"top_traits_explanation": "Your engagement patterns show...",
"archetype": "Strategic Thinker",
"nudges": [...]
},
"connectedPlatforms": ["linkedin", "youtube"]
}
```
| Field | Description |
|-------|-------------|
| `traits.user_summary` | 2-3 paragraph personality summary |
| `traits.top_traits_explanation` | Why these traits were identified |
| `traits.archetype` | 2-3 word personality archetype |
| `traits.nudges` | Personalized action suggestions |
| `userProfile` | Convenient access to profile fields |
---
When a user authorizes your app, their traits may need to be generated (training). You have two options:
1. **Wait Mode** (default): User sees loading screen while training completes
2. **Background Mode**: SDK returns immediately, you poll for completion
### Web npm SDK (`backgroundLoadData`)
On **`OnairosButton`**, set **`backgroundLoadData={true}`** with **`autoFetch={true}`** so that after connections/PIN/consent the SDK **skips the built-in training loading screen**, navigates to the data-request step, and completes with **`onComplete({ apiUrl, token, training, authorizedData, ... })`** while traits may still be generating. The SDK starts training in the background (socket **`start-training`**) so status polling is meaningful. Your app should:
1. Read **`training.ready`**. If `true`, **`POST apiUrl`** with `Authorization: Bearer <token>`.
2. If `false`, call **`pollTrainingStatus`** from **`onairos`** (or your own loop) on **`training.statusUrl`** until ready, then **`POST apiUrl`**.
See **`docs/CROSS_SDK_PARITY.md`** and **`docs/ONAIROS_BUTTON_PROPS.md`** for the full matrix with **`autoFetch`**.
### When to Use Background Mode
- User experience is time-sensitive
- You want to show your app UI immediately
- You'll fetch traits later (e.g., on next screen)
---
## 4. Implementation Guide
### Step 1: Check if Traits Are Ready
After authorization, check `training.ready`:
```javascript
const authResult = await onairosSDK.authorize();
if (authResult.training.ready) {
// Traits exist - fetch immediately
const userData = await fetchTraits(authResult.apiUrl, authResult.token);
displayUserData(userData);
} else {
// Training in progress - poll for completion
showLoadingState();
const userData = await pollForTraits(authResult);
displayUserData(userData);
}
```
```javascript
async function fetchTraits(apiUrl, token) {
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`Failed to fetch traits: ${response.status}`);
}
return await response.json();
}
```
Use each response’s **`polling.recommendedIntervalMs`** when present; otherwise fall back to **`training.pollIntervalMs`** from the authorize payload. On **HTTP 429**, honor **`Retry-After`** (header or seconds) and any **`recommendedIntervalMs`** / **`retryAfter`** in the JSON body before retrying.
```javascript
async function pollForTraits(authResult) {
const { apiUrl, token, training } = authResult;
const { statusUrl, pollIntervalMs } = training;
const maxWaitMs = 300000; // 5 minutes max
const startTime = Date.now();
while (Date.now() - startTime < maxWaitMs) {
const res = await fetch(statusUrl, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (res.status === 429) {
const errBody = await res.json().catch(() => ({}));
const retryAfterHeader = res.headers.get('Retry-After');
const waitSec = retryAfterHeader ? parseInt(retryAfterHeader, 10) : (errBody.retryAfter ?? 5);
await sleep(Math.max((waitSec || 5) * 1000, errBody.recommendedIntervalMs || 0));
continue;
}
const status = await res.json();
// Update UI with progress (optional)
if (status.progress) {
updateProgressBar(status.progress.progressPercent);
updateETA(`${status.progress.remainingSeconds}s remaining`);
updateMessage(status.progress.message);
}
// Check if training complete
if (!status.trainingStatus.isCurrentlyTraining) {
// Training done! Fetch traits
return await fetchTraits(apiUrl, token);
}
const waitMs = status.polling?.recommendedIntervalMs ?? pollIntervalMs ?? 5000;
await sleep(waitMs);
}
throw new Error('Training timeout - please try again');
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
```
---
```
GET https://api2.onairos.uk/training/status
Authorization: Bearer <token>
```
Legacy path (same behavior): `GET https://api2.onairos.uk/mobile-training/status`
### Response (During Training)
```json
{
"success": true,
"trainingStatus": {
"isCurrentlyTraining": true,
"canTrainAgain": false,
"lastTrainingDate": "2026-03-09T05:30:00Z",
"minutesSinceLastTraining": 2
},
"progress": {
"progressPercent": 45,
"elapsedSeconds": 54,
"estimatedTotalSeconds": 120,
"remainingSeconds": 66,
"stage": "processing",
"message": "Analyzing your data..."
},
"polling": {
"recommendedIntervalMs": 3000,
"maxPollTimeMs": 300000
}
}
```
```json
{
"success": true,
"trainingStatus": {
"isCurrentlyTraining": false,
"canTrainAgain": true,
"lastTrainingDate": "2026-03-09T05:32:00Z"
},
"progress": null,
"polling": {
"recommendedIntervalMs": 10000,
"maxPollTimeMs": 300000
}
}
```
| Field | Type | Description |
|-------|------|-------------|
| `trainingStatus.isCurrentlyTraining` | boolean | `true` = still training, keep polling |
| `trainingStatus.canTrainAgain` | boolean | Whether user can retrain |
| `progress.progressPercent` | number | 0-100, completion percentage |
| `progress.remainingSeconds` | number | Estimated seconds until complete |
| `progress.message` | string | Human-readable status message |
| `polling.recommendedIntervalMs` | number | How often to poll (ms) |
---
## 6. Rate Limits
| Endpoint | Limit | Response if Exceeded |
|----------|-------|---------------------|
| `/training/status` (legacy: `/mobile-training/status`) | 30 requests/minute | `429 Too Many Requests` |
If you receive a `429` response:
```json
{
"success": false,
"error": "Too many requests. Please slow down polling.",
"retryAfter": 60,
"recommendedIntervalMs": 5000
}
```
Check the `Retry-After` header and wait before retrying.
---
```javascript
import { useState, useEffect } from 'react';
function OnairosIntegration() {
const [userData, setUserData] = useState(null);
const [loading, setLoading] = useState(false);
const [progress, setProgress] = useState(null);
const [error, setError] = useState(null);
async function handleAuthorization(authResult) {
setLoading(true);
setError(null);
try {
if (authResult.training.ready) {
// Traits ready - fetch immediately
const data = await fetchTraits(authResult.apiUrl, authResult.token);
setUserData(data);
} else {
// Training in progress - poll
const data = await pollForTraits(authResult);
setUserData(data);
}
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
setProgress(null);
}
}
async function fetchTraits(apiUrl, token) {
const response = await fetch(apiUrl, {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` }
});
return response.json();
}
async function pollForTraits(authResult) {
const { apiUrl, token, training } = authResult;
const maxWait = 300000;
const start = Date.now();
while (Date.now() - start < maxWait) {
const status = await fetch(training.statusUrl, {
headers: { 'Authorization': `Bearer ${token}` }
}).then(r => r.json());
// Update progress UI
if (status.progress) {
setProgress({
percent: status.progress.progressPercent,
message: status.progress.message,
eta: status.progress.remainingSeconds
});
}
// Check if done
if (!status.trainingStatus.isCurrentlyTraining) {
return await fetchTraits(apiUrl, token);
}
// Wait
await new Promise(r => setTimeout(r, status.polling.recommendedIntervalMs));
}
throw new Error('Training timeout');
}
return (
<div>
{loading && progress && (
<div className="progress">
<div className="progress-bar" style={{ width: `${progress.percent}%` }} />
<p>{progress.message}</p>
<p>ETA: {progress.eta} seconds</p>
</div>
)}
{userData && (
<div className="user-profile">
<h2>{userData.userProfile.archetype}</h2>
<p>{userData.userProfile.user_summary}</p>
<h3>Your Traits</h3>
{Object.entries(userData.traits.positive_traits).map(([trait, score]) => (
<div key={trait}>
<span>{trait}</span>
<span>{score}%</span>
</div>
))}
</div>
)}
{error && <p className="error">{error}</p>}
</div>
);
}
```
```python
import time
import requests
def get_user_traits(auth_result):
"""
Get user traits, polling if training is in progress.
"""
api_url = auth_result['apiUrl']
token = auth_result['token']
training = auth_result['training']
headers = {'Authorization': f'Bearer {token}'}
if training['ready']:
response = requests.post(api_url, headers=headers)
return response.json()
status_url = training['statusUrl']
poll_interval = training['pollIntervalMs'] / 1000
max_wait = 300
start_time = time.time()
while time.time() - start_time < max_wait:
status = requests.get(status_url, headers=headers).json()
if status.get('progress'):
print(f"Progress: {status['progress']['progressPercent']}%")
print(f"ETA: {status['progress']['remainingSeconds']}s")
if not status['trainingStatus']['isCurrentlyTraining']:
response = requests.post(api_url, headers=headers)
return response.json()
time.sleep(status['polling']['recommendedIntervalMs'] / 1000)
raise TimeoutError('Training timeout')
# Usage
auth_result = onairos_sdk.authorize()
user_data = get_user_traits(auth_result)
print(f"Archetype: {user_data['userProfile']['archetype']}")
print(f"Summary: {user_data['userProfile']['user_summary']}")
```
---
If you were using the old API format:
- [ ] Remove references to `userData.email` (no longer available)
- [ ] Remove references to `userData.username` (no longer available)
- [ ] Update to use `training.ready` instead of `userData.isReturningUser`
- [ ] Implement polling logic for `training.ready === false`
- [ ] Update trait parsing to use new `userProfile` structure
- [ ] Handle rate limiting (429 responses)
---
## 9. FAQ
### Q: Why was email/username removed?
**A:** For security and privacy. The user consented to share their traits with your app, not their email address. Use the internal user ID if you need to track users.
### Q: How long does training take?
**A:** Typically 60-120 seconds for first-time users. The `progress.estimatedTotalSeconds` field provides a real-time estimate.
### Q: What if training fails?
**A:** Check `trainingStatus.canTrainAgain`. If true, the user can re-authorize to retry training.
### Q: Can I skip polling and just wait?
**A:** Yes, if your SDK is in "wait mode" (backgroundMode: false), the SDK handles this internally and returns traits directly.
### Q: What's in the `userProfile` object?
**A:**
- `user_summary`: 2-3 paragraph personality description
- `top_traits_explanation`: Why these traits were identified
- `archetype`: 2-3 word personality label (e.g., "Strategic Thinker")
- `nudges`: Personalized suggestions/tips
---
## 10. Support
For questions or issues:
- Email: support@onairos.uk
- Documentation: https://docs.onairos.uk
- Status: https://status.onairos.uk